套接字编程
-
地址转换函数
inet_addr
:in_addr_t inet_addr(const char *strptr)
将点分十进制的IP转换为长整数类型(u_long)inet_ntoa
:char* inet_ntoa(struct in_addr in)
将十进制网络字节序转换为点分十进制IP格式的字符串inet_pton
:int inet_pton(int af, const char *src, void *dst)
, af表示地址簇(AF_INET为ipv4, AF_INET6表示ipv6) 将IP地址将 点分十进制 与 二进制整数 之间转换, 能够处理ipv4与ipv6inet_ntop
:const char* inet_ntop(int af, const void* src, char *dst, socklen_t cnt)
, cnt指向缓存区dst的大小 将二进制整数转换为点分十进制, 能够处理ipv4与ipv6套接字地址结构
在内核和进程之间进行传递
1. IPv4: struct sockaddr_in, 16个字节
1 struct sockaddr_in { 2 sa_family_t sin_family; /* AF_INET */ 3 in_port_t sin_port; /* Port number. */ 4 struct in_addr sin_addr; /* Internet address. */ 5 6 /* Pad to size of `struct sockaddr'. */ 7 unsigned char sin_zero[sizeof (struct sockaddr) - 8 sizeof (sa_family_t) - 9 sizeof (in_port_t) - 10 sizeof (struct in_addr)]; 11 }; 12 typedef uint32_t in_addr_t; 13 struct in_addr { 14 in_addr_t s_addr; /* IPv4 address */ 15 };
2. IPv6: struct sockaddr_in6,28个字节
1 struct sockaddr_in6 { 2 sa_family_t sin6_family; /* AF_INET6 */ 3 in_port_t sin6_port; /* Transport layer port # */ 4 uint32_t sin6_flowinfo; /* IPv6 flow information */ 5 struct in6_addr sin6_addr; /* IPv6 address */ 6 uint32_t sin6_scope_id; /* IPv6 scope-id */ 7 }; 8 struct in6_addr { 9 union { 10 uint8_t u6_addr8[16]; 11 uint16_t u6_addr16[8]; 12 uint32_t u6_addr32[4]; 13 } in6_u; 14 15 #define s6_addr in6_u.u6_addr8 16 #define s6_addr16 in6_u.u6_addr16 17 #define s6_addr32 in6_u.u6_addr32 18 };
3. 通用结构体1: struct sockaddr, 16个字节
1 struct sockaddr { 2 sa_family_t sa_family; /* Address family */ 3 char sa_data[14]; /* protocol-specific address */ 4 };
4. 通用结构体2: struct sockaddr_storage,128个字节
1 /* Structure large enough to hold any socket address 2 (with the historical exception of AF_UNIX). 128 bytes reserved. */ 3 4 #if ULONG_MAX > 0xffffffff 5 # define __ss_aligntype __uint64_t 6 #else 7 # define __ss_aligntype __uint32_t 8 #endif 9 #define _SS_SIZE 128 10 #define _SS_PADSIZE (_SS_SIZE - (2 * sizeof (__ss_aligntype))) 11 12 struct sockaddr_storage 13 { 14 sa_family_t ss_family; /* Address family */ 15 __ss_aligntype __ss_align; /* Force desired alignment. */ 16 char __ss_padding[_SS_PADSIZE]; 17 };
身为一个通用的地址数据结构,它的大小得要是所有具体协议地址结构大小的最大值,可是
sizeof(struct sockaddr) = 16
, 而izeof(struct sockaddr_in6) = 28
, 显然struct sockaddr
这个通用的数据结构hold不住IPv6啊,所以struct sockaddr_storage
这个新结构横空出世了,它的大小为128字节,应该能装得下目前所以协议的地址结构了。IPv4/IPv6混合编程示例:
1 struct sockaddr_storage addr; 2 memset(&addr, 0, sizeof(struct sockaddr_storage)); 3 if (isIPv6 == TRUE) 4 { 5 struct sockaddr_in6 *addr_v6 = (struct sockaddr_in6 *)&addr; 6 addr_v6->sin6_family = AF_INET6; 7 addr_v6->sin6_port = 1234; 8 inet_pton(AF_INET6, “2001:3211::1”, &(addr_v6->sin6_addr)); 9 } 10 else 11 { 12 struct sockaddr_in *addr_v4 = (struct sockaddr_in *)&addr; 13 addr_v4->sin_family = AF_INET; 14 addr_v4->sin_port = 1234; 15 inet_aton(“192.168.1.228”, &(addr_v4->sin_addr)); 16 } 17 18 sendto(sock, buf, len, 0, (struct sockaddr *)&addr, sizeof(struct sockaddr_storage));
主机字节序与网络字节序
-
网络字节序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输室能够被正确解释。网络字节序采用big endian排序方式。
-
主机字节序就是我们常说的大端和小端模式,不同的CPU有不同的字节序类型,
字节序指的是整数在内存中保存的顺序。最常见的有两种:
Little endian:将低序字节存储在起始地址,低位存放在内存地址小的地方,符合人的思维
Big endian:将高序字节存储在起始地址,地址低位存储值的高位,较为直观
#indlude <netinet/in.h> // 主机字节序转网络字节序 uint16_t htons(uint16_t host16bitvalue); uint32_t htonl(uint32_t host32bitvalue); // 网络字节序转主机字节序 uint16_t ntohs(uint16_t net16bitvalue); uint32_t ntohl(uint32_t net32bitvalue); // h表示host,n表示network,s表示short,l表示long // s表示端口号的转换,l转换IP地址
-
地址转换函数
值-结果参数
套接字地址结构的传递方向
-
从进程到内核
3个函数(
bind,connect,sendto
)函数参数为指向某个套接字地址结构的指针,另一个参数为套接字地址结构的大小(
socklen_t
)将指针与指针所指的内容大小传递给内核,内核就知道了应该从进程中复制多少数据
-
从内核到进程
4个函数(
accept,recvfrom,getsockname,getpeername
)其中两个参数为指向某个套接字地址结构的指针和指向表示该结构大小的整数变量的指针调用函数时,指向结构大小是一个值,告知内核该结构的大小,使读取时不会越界函数返回时,结构大小是一个结果,它告知进程内核在该结构中存储了多少信息。这种类型的参数称为值-结果参数(value-result)。对于可变长度的套接字地址结构,返回值可能与传入的值不同。值-结果参数
-
select函数中间的三个参数
-
getsockopt函数的长度参数
-
ifconf结构中的ifc_len字段
-
sysctl函数两个长度参数中的第一个
-
字节流与字符流
字符流为2个字节的Unicode字符,对多国语言的支持性比较好。所有文件都是以字节形式存储在磁盘上的,在磁盘上存储的并不是文件的字符而是先将字符编码为字节,再将字节存储到磁盘。
字节流可用于存储任何类型的对象,包括二进制对象,而字符流只能处理字符或字符串字节流提供了处理任何类型的IO操作功能,但它不能直接处理Unicode字符,但字符流可以。
字节流在操作的时候本身是不会用到缓冲区(内存)的,是与文件本身直接操作的,而字符流在操作的时候是使用到缓冲区的。字节流在操作文件时,即使不关闭资源(close方法),文件也能输出,但是如果字符流不使用close方法的话,则不会输出任何内容,说明字符流用的是缓冲区,并且可以使用flush方法强制进行刷新缓冲区,这时才能在不close的情况下输出内容
主动套接字与被动套接字
当socket
函数创建套接字时,它被假设为一个主动套接字(active socket)。将调用connect发起连接。
当调用了listen
函数后,将会转变为被动套接字(passive socket)。用于指示内核接受指向该套接字的连接请求。调用listen
之后导致套接字从CLOSED
状态转换至LISTEN
状态。
监听套接字队列
-
未完成连接队列
某个客户端发出请求并到达服务器,而服务器正在完成响应的TCP三次握手过程。此时这些套接字处于SYN_RECV状态
-
已完成连接队列
每个已完成TCP三次握手过程的客户端套接字。此时这些套接字处于ESTABLISHED状态
TCP套接字状态
TCP套接字共有11中状态。
CLOSED
LISTEN:表示被动打开
SYN_SEND:表示主动打开,客户端发送SYN(J)
SYN_RECV:被动打开,服务器接收到SYN(J)并回复SYN(K)+ACK(J+1)
ESTABLISHED:客户端接收到SYN(K)+ACK(J+1),服务端接收到ACK(K+1)
FIN_WAIT_1:主动关闭,先发送FIN(M)的一端
FIN_WAIT_2:主动关闭,接收到另一端的ACK(M+1)
TIME_WAIT:被动关闭,接收到另一端的FIN(M)
CLOSING:主动关闭,两端同时发送FIN会出现
CLOSE_WAIT:主动关闭,接收到被动端的FIN(N)
LAST_ACK:被动关闭,接收到主动端的FIN(N+1)
套接字描述符
服务器会有两个套接字描述符,分别为监听套接字(listenfd)和已连接套接字(connfd)。
而listenfd套接字描述符对应的地址为 绑定的通配IP地址和指定的端口connfd套接字描述符对应 连接的服务器端的地址 ,实际上为内核指定的地址和端口号。
相当于用getsockname函数得到的listenfd的地址和端口为我们设定的服务器地址和端口号,而connfd所得到的是内核自己分配的。
并发服务器
-
使用多线程实现并发服务器----fork
-
当fork子进程时,必须捕获SIGCHLD信号;(SIGCHLD,在一个进程终止或者停止时,将SIGCHLD信号发送给其父进程,按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号 )
-
当捕获信号时,必须处理被中断的系统调用;EINTR 表示被中断
-
SIGCHLD的信号函数必须正确编写,应使用waitpid函数以免留下僵死进程。
-
pid_t pid;
int listenfd, connfd;
struct sockaddr_in servaddr, cliaddr;
socklen_t clilen = sizeof(cliaddr);
listenfd = socket(AF_INET, SOCK_STREAM, 0);
memset(servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 通配地址:若系统是多宿主机,将接受目的地址为任何本地接口的连接
bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr));
listen(listenfd, backlog);
signal(SIGCHLD, sigchld); // 捕获SIGCHLD信号
for( ; ; ) {
if (connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen) < 0) {
if (errno = EINTR) // 处理accept被中断,continue之后重启被中断的系统调用
continue;
else
printf("accept error");
}
if ((pid = fork()) == 0) { // 子进程
close(listenfd); // 子进程关闭监听套接字,只对已连接套接字进行处理
process(connfd);
close(connfd);
exit(0);
}
close(connfd); // 父进程关闭已连接套接字,因为处理步骤已经在子进程中进行,而父进程需要一直监听有没有新的连接到达
}
-
使用多线程实现
/*
TO DO
*/
-
使用IO阻塞实现
/*
TO DO
*/