prerequisite
有语言基础;大概学过计网;下面的代码都是在Linux
平台的,最好懂点Linux
;
basic concept
socket
提供了两种通信机制,stream
流和datagram
数据报stream socket
基于TCP
,所以可靠datagram socket
基于UDP
,所以不可靠但是高效- C语言的这一套东西写的非常早,那时候还没有
void*
,所以结构体什么的都定义的比较乱。 - 下面的内容只讲流
socket
,数据报socket
应用场景较少(比如实时音视频,允许丢包),而且可能会越来越少。
网络字节序
- 大端序Big Endian,高位数据放在内存低地址,低位数据放在内存高地址
- 小端序Little Endian,高位数据放在内存高地址,低位数据放在内存低地址
- 网络字节序采用大端序
- 字节序转换函数,
htons(uint16), htonl(uint32), ntohs(uint16), ntohl(uint32)
,n-network; h-host; s-short-16bit; l-long-32bit
socket()函数
#include<sys/socket.h> // 头文件
int socket(int af, int type, int protocol); //函数原型
int tcp_socket_fd = socket(AF_INET, SOCK_STREAM, 0); //创建stream socket,返回一个file descriptor
if(tcp_socket_fd == -1) {perror("create socket failed."); return -1;} //最好判断一下fd
af
为地址族(Address Family),也就是IP
地址类型,常用的有AF_INET
和AF_INET6
。INET
是Internet
的简写。AF_INET
表示IPv4
地址;AF_INET6
表示IPv6
地址。你也可以使用PF
前缀,PF
是Protocol Family
的简写,它和AF
是一样的。type
为数据传输方式/socket
类型,常用的有SOCK_STREAM
流和SOCK_DGRAM
数据报。protocol
表示传输协议,常用的有IPPROTO_TCP
和IPPROTO_UDP
,分别表示TCP
传输协议和UDP
传输协议。有了地址类型和数据传输方式这前两个参数,就能够确定协议了,所以此时这个参数可以写0。- 同一个文件中能打开的
socket
的数量是有限的,linux
一切皆文件,使用命令ulimit -a
查看open file
的数量限制。 - 服务端和客户端第一步都需要用这个函数建立
socket
几个结构体
#include<sys/socket.h>
struct sockaddr //存IP和端口,这个结构体用的不多,因为有下面的sockaddr_in替代它
{
uint16_t sa_family; //2 Bytes, 地址类型,比如AF_INET
char sa_data[14]; //14B, 存放地址和端口
};
#include<netinet/in.h>或#include<arpa/inet.h>
struct sockaddr_in //存IP和端口
{
uint16_t sin_family; //2B 地址类型
uint16_t sin_port; //2B 端口号
struct in_addr sin_addr; //地址
unsigned char sin_zero[8]; //为了和sockaddr保持一致大小,实际并不使用,大小一致就可以强制转换
};
struct in_addr
{
uint32_t s_addr; //4B 地址
};
#include<netdb.h>
#include<sys/socket.h>
struct hostent //short for host entry
{
char *h_name; //host name
char **h_aliases; //主机所有别名构成的字符串数组,同一IP可绑定多个域名
int h_addrtype; //host address type IPv4? IPv6?
int h_length; //host IP address length(bytes), ipv4-4B, ipv6-16B
char **h_addr_list; //host IP address 网络字节序
};
#define h_addr h_addr_list[0]
struct hostent *gethostbyname(const char *name); //这个函数可由字符串格式的域名得到网络字节序的IP地址
地址转换函数
int inet_aton(const char *cp, struct in_addr *inp);
/*将一个字符串IP(cp)转为32位的网络字节序IP存在结构体inp中。成功,返回值非0,IP地址不正确导致失败会返回0*/
char *inet_ntoa(struct in_addr in);
//将网络字节序IP转为字符串IP
int_addr_t inet_addr(const char *cp);
//inet_addr函数将点分十进制字符串的IP地址转为网络字节序的二进制值
sockaddr_in结构体使用
struct sockaddr_in serv_addr; //以服务器端为例,后面bind函数用到了这个变量
memset(&serv_addr, 0, sizeof serv_addr); //主要是为了把sin_zero置为0
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr("192.168.1.1"); //指定IP地址
//serv_addr.sin_addr.s_addr = htonl(INADDR_ANY) //任意IP地址
serv_addr.sin_port = htons(5005); //随便指定一个高端口5005
bind函数
int bind(int sock, struct sockaddr *addr, socklen_t addrlen); //函数原型
//使用的例子
if(bind(sock_fd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) != 0)
{
perror("bind failed.");
close(sock_fd); //sock_fd 是socket函数返回的socket file descriptor
return -1;
}
- 服务器端第二步的操作,要用
bind
函数将socket
与特定的IP
地址和端口绑定起来,这样,流经该IP
地址和端口的数据才能交给socket
处理 sock
为socket
文件描述符,addr
为sockaddr
结构体变量的指针,addrlen
为addr
变量的大小serv_addr
是前面sockaddr_in
结构体变量,bind
函数参数是sockaddr
结构体,需要强制转换一下,否则会报错- 端口只能使用
1024
以上的非熟知的高端口,最大到65535,即short
的最大值 1024
以下的端口,root
权限可以使用- 一个端口占用后,不能重复占用
- 端口释放之后会有两分钟时间不可用,处于
TIME_WAIT
状态,可以用SO_REUSEADDR
取消等待时间,具体不讲了 bind
成功返回0
,否则返回-1
,errno
存错误编号
listen
#include<sys/socket.h>
int listen(int s, int backlog); //函数原型
// 使用实例
if(listen(sock_fd, 3) != 0)
{
perror("listen failed");
close(sock_fd);
return -1;
}
- 服务器端第三步的操作,把
socket
设置为监听模式。 - 第一个参数是前面已经
bind
的socket fd
- 第二个参数指定同时能处理的最大连接请求, 如果连接数目达此上限则客户端将收到连接拒绝的错误
- 接收的连接会放到一个队列里
- 成功返回
0
, 失败返回-1
,错误原因存于errno
accept
int accept(int s, struct sockaddr * addr, int * addrlen); //函数原型
//使用实例
int sock_len = sizeof(struct sockaddr_in);
struct sockaddr_in client_addr;
int client_fd = accept(sock_fd, (struct sockaddr*)&client_addr, (socklen_t*)&sock_len);
- 服务器第四步,用
accept
函数接收客户端的连接 - 第一个参数是监听模式的
socket fd
- 第二个参数是用于存放客户端地址的结构体变量指针,同样需要强制转换一下
- 第三个参数是结构体
sockaddr
的长度 - 前面说,
listen
监听到的连接请求会放到一个队列里,accept
会从队列里取出一个建立连接,若队列为空,则accept
会阻塞 - 成功返回一个新的
socket fd
,后续通信就使用这个fd
,原来的sock_fd
可以使用accept
接收新的连接请求 accept4()
函数在最后增加了一个参数,可以设置 socket 的属性
connect
int connect(int sock, struct sockaddr *serv_addr, socklen_t addrlen); //函数原型
- 客户端第二步操作,用
connect
函数与服务器建立连接,服务器必须listen
之后才能连接成功 - 第一个参数是
socket
函数返回的socket fd
- 第二个参数是存服务器端的地址的结构体指针,需要在
connect
之前写好这个结构体,同样使用sockaddr_in
结构体然后强制转换类型。 - 第三个参数是结构体的长度
- 连接成功返回
0
,失败返回-1
,错误原因存于errno
- 因为使用的是
stream socket
,所以客户端的connect
和服务器端的accept
在底层会发生TCP
三次握手。 - 三次握手中,客户端发来
SYN
包后,这个连接就处于半连接状态SYN_RECV
。服务器再发送SYN ACK
,客户端接收,再发来ACK
,连接状态改为ESTABLISHED
连接建立。 - 服务器
listen
函数第二个参数backlog
设定的就是ESTABLISHED
状态队列的长度。半连接队列和全连接队列可以看这篇文章
send
int send(int s, const void * msg, int len, unsigned int falgs); //函数原型
- 连接建立之后,服务端和客户端都使用
send
函数发送数据 - 第一个参数为建立连接的
socket fd
- 第二个参数为要发送的数据的内存起始地址,可以是基本数据类型也可以是结构体、数组等,内存里有什么就发送什么,大于
1
个字节需要转换字节序 - 第三个参数是发送的数据长度
- 第四个参数一般设为
0
,其他的意义不大。MSG_OOB
, 传送的数据以out-of-band
送出;MSG_DONTROUTE
, 取消路由表查询;MSG_DONTWAIT
, 设置为不可阻断运作;MSG_NOSIGNAL
, 此动作不愿被SIGPIPE
信号中断. - 函数成功返回实际发送的字节数,失败返回
-1
,错误信息存于errno
- 发送数据实际是往缓冲区里写,若发的快,对端接收慢的话,缓冲区会被填满,此时
send
就会阻塞
recv
int recv(int s, void *buf, int len, unsigned int flags);
- 连接建立之后,服务端和客户端都使用
recv
函数接收数据 - 参数与
send
函数类似,接收的数据存到buf
指向的地址,可接受最大长度为len
,flag
一般设0
- 成功返回接收的字节数,失败返回
-1
,错误信息存于errno
- 接收数据是从缓冲区里读,读不到就会阻塞
addrinfo struct
struct addrinfo
{
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
char *ai_canonname;
size_t ai_addrlen;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
};
int getaddrinfo(const char *hostname, /* hostname or address */
const char *service, /* port or service name */
const struct addrinfo *hints, /* input parameters */
struct addrinfo **result ); /* output linked list */
- given host and service,
getaddrinfo
returns result that points to a linked list ofaddrinfo
structs, each of which points to corresponding socket address struct, and which contains arguments for the sockets interface functions - 前两个参数一般用一个,另一个为空
void freeaddrinfo(struct addrinfo *result); /* free linked list */
int getnameinfo(const struct sockaddr *sa, socklen_t salen, /* in: socket addr*/
char *host, size_t hostlen, /*out: host*/
char *serv, size_t servlen, /*out: service*/
int flags); /*optional flags*/
封装
如果每个项目都从底层socket
开始写的话,效率很低,工作量很大,解决这个问题需要封装(造轮子)。把常用的代码封装一下,以后直接调用就行了。要保证安全的话可以使用OpenSSL
。关于OpenSSL
编程的资料网上很少,只看man
的话是很难学的。