在socket编程中有一个通用的结构体来表示通用的表示一个网络中某个主机的地址struct sockaddr
1 2 3 4 5 6 struct sockaddr /* socket_address */{ sa_family_t sa_family; char sa_data[14 ]; }
这个结构体可以表示所有类型的网络,包括ipv4。因为比较普遍,所以socket中的大部分API中有关主机地址的参数类型都是它。就像父类一样,用多态的方式使用。
此外,使用地址类型的参数时还会有个socklen_t addrlen参数和它配套使用,表示地址变量的大小(bytes)
因为现在大多用的是ipv4协议,所以专门有一个类型表示ipv4地址,它的内存大小与struct sockaddr完全一样,内存布局也相似,完全就是它的子类一样
套接字-Socket | 爱编程的大丙 (subingwen.cn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef unsigned int uint32_t ;typedef unsigned short uint16_t ;typedef unsigned short int sa_family_t ;typedef uint16_t in_port_t ;struct in_addr { uint32_t s_addr; }; typedef unsigned int socklen_t ;struct sockaddr_in /* socket_address_internet */{ sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; unsigned char sin_zero[8 ]; };
用到的时候需要传入指针,并且还需要像多态一样将其强制类型转化为其父类的指针,此外还需要配套一个socklen_t addrlen表示其大小。就像下面这样
1 2 3 struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad);accept(welcomeSocket, (struct sockaddr*)&cad, &cadlen);
网络字节序要求是大端发送的,所以跟socket相关的结构体尤其是struct sockaddr_in里面的port和ip地址要求都是大端存储的。为了实现对整数大小端的自由转换提供了2 组4 个函数,分别处理2个字节和4个字节大小的数据的大小端转换。
使用套接字通信函数需要包含头文件<arpa/inet.h>,包含了这个头文件<sys/socket.h>就不用在包含了。
1 2 3 4 5 6 7 8 9 10 11 12 #include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; uint16_t ntohs (uint16_t netshort) ; uint32_t htonl (uint32_t hostlong) ; uint32_t ntohl (uint32_t netlong) ;
IP地址一般都是点分十进制字符串形式出现的,但其本质上还是32位4字节大小的整数(对IPv4而言 ),所以还提供了针对IP地址(包括IPv4和IPv6)字符串和整数的转换函数。值得注意的是,转换的整数(不管的作为参数还是返回结果)都应该是大端序存储的。
n : Network (网络格式,也就是二进制格式(大端序整数格式 ),通常是大端序的表示方式)
p : Presentation (文本格式 ,适合人类阅读的形式,如 "192.168.1.1")
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <arpa/inet.h> const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;参数: af: 地址族协议 AF_INET: ipv4格式的ip地址 AF_INET6: ipv6格式的ip地址 src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址 dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址 size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节 返回值: 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串 失败: NULL
1 2 3 4 5 6 7 8 9 10 #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ; 参数: af: 地址族(IP地址的家族包括ipv4和ipv6)协议 AF_INET: ipv4格式的ip地址 AF_INET6: ipv6格式的ip地址 src: 传入参数, 对应要转换的点分十进制的ip地址: 192.168 .1.100 dst: 传出参数, 函数调用完成, 转换得到的大端整形IP被写入到这块内存中 返回值:成功返回1 ,失败返回0 或者-1
客户端通过命令行向服务器发送数据,按下回车后会将换行符一起发送给服务器。可能是\r\n也可能是\n视操作系统不同吧
客户端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { const char * SERVER_IP_ADDRESS = "192.168.66.142" ; const unsigned short int SERVER_PORT = 8080 ; uint32_t SERVER_IP; inet_pton(AF_INET, SERVER_IP_ADDRESS, &SERVER_IP); int res; struct sockaddr_in sad ; memset (&sad, 0 , sizeof (sad)); sad.sin_family = AF_INET; sad.sin_port = htons(SERVER_PORT); sad.sin_addr.s_addr = SERVER_IP; int clientSocket = socket(AF_INET, SOCK_STREAM, 0 ); if (clientSocket == -1 ) { perror("clientSocket创建失败" ); return -1 ; } res = connect(clientSocket, (struct sockaddr*)&sad, sizeof (sad)); if (res == -1 ) { perror("与服务器建立连接失败" ); return -1 ; } char buf[1024 ] = {0 }; int i = 0 ; while (1 ) { sprintf (buf, "%d: Hello World\n" , i++); send(clientSocket, buf, strlen (buf) + 1 , 0 ); memset (buf, 0 , sizeof (buf)); int len = recv(clientSocket, buf, sizeof (buf), 0 ); if (len > 0 ) { printf ("服务器: %s" , buf); } else if (len == 0 ) { printf ("服务器断开连接...\n" ); break ; } else { perror("从服务器接收数据失败" ); break ; } sleep(1 ); } close(clientSocket); return 0 ; }
服务器端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { const unsigned short int PORT = 8080 ; int res; struct sockaddr_in sad ; memset (&sad, 0 , sizeof (sad)); sad.sin_family = AF_INET; sad.sin_port = htons(PORT); sad.sin_addr.s_addr = htonl(INADDR_ANY); int welcomeSocket = socket(AF_INET, SOCK_STREAM, 0 ); if (welcomeSocket == -1 ) { perror("welcomeSocket创建失败" ); return -1 ; } res = bind(welcomeSocket, (struct sockaddr*)&sad, sizeof (sad)); if (res == -1 ) { perror("welcomeSocket绑定失败" ); return -1 ; } res = listen(welcomeSocket, 128 ); if (res == -1 ) { perror("监听失败" ); return -1 ; } struct sockaddr_in cad ; memset (&cad, 0 , sizeof (cad)); socklen_t cadlen = sizeof (cad); int connectionSocket = accept(welcomeSocket, (struct sockaddr*)&cad, &cadlen); if (connectionSocket == -1 ) { perror("与客户端建立连接失败" ); return -1 ; } char cip[32 ]; inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); unsigned short int cport = ntohs(cad.sin_port); printf ("与客户端[%s:%d]建立连接成功\n" , cip, cport); char buf[1024 ] = {0 }; while (1 ) { int len = recv(connectionSocket, buf, sizeof (buf) - 1 , 0 ); if (len > 0 ) { printf ("客户端: %s" , buf); send(connectionSocket, buf, len, 0 ); } else if (len == 0 ) { printf ("客户端断开连接...\n" ); break ; } else { perror("从客户端接收数据失败" ); break ; } } close(welcomeSocket); close(connectionSocket); return 0 ; }
多线程简单高并发服务器端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 #include <stdio.h> #include <unistd.h> #include <string.h> #include <pthread.h> #include <arpa/inet.h> #define UNCONNECTED -1 #define NR_CLIENTS 128 struct client { int connectionSocket; struct sockaddr_in address ; }; struct client clients [NR_CLIENTS ];void * thread (void * arg) { struct client * client = (struct client*)arg; char cip[32 ]; inet_ntop(AF_INET, &client->address.sin_addr.s_addr, cip, sizeof (cip)); unsigned short int cport = ntohs(client->address.sin_port); printf ("与客户端[%s:%d]连接成功\n" , cip, cport); char buf[1024 ] = {0 }; while (1 ) { int len = recv(client->connectionSocket, buf, sizeof (buf) - 1 , 0 ); if (len > 0 ) { printf ("客户端[%s:%d]: %s" , cip, cport, buf); send(client->connectionSocket, buf, len, 0 ); } else if (len == 0 ) { printf ("客户端[%s:%d]断开连接\n" , cip, cport); break ; } else { char error[128 ]; sprintf (error, "从客户端[%s:%d]接收数据失败" , cip, cport); perror(error); break ; } } close(client->connectionSocket); memset (client, 0 , sizeof (struct client)); client->connectionSocket = UNCONNECTED; return NULL ; } int main () { const unsigned short int PORT = 8080 ; int i; int res; for (i = 0 ; i < NR_CLIENTS; i++) { memset (&clients[i], 0 , sizeof (struct client)); clients[i].connectionSocket = UNCONNECTED; } struct sockaddr_in sad ; memset (&sad, 0 , sizeof (sad)); sad.sin_family = AF_INET; sad.sin_port = htons(PORT); sad.sin_addr.s_addr = htonl(INADDR_ANY); int welcomeSocket = socket(AF_INET, SOCK_STREAM, 0 ); if (welcomeSocket == -1 ) { perror("welcomeSocket创建失败" ); return -1 ; } res = bind(welcomeSocket, (struct sockaddr*)&sad, sizeof (sad)); if (res == -1 ) { perror("welcomeSocket绑定失败" ); return -1 ; } res = listen(welcomeSocket, 128 ); if (res == -1 ) { perror("监听失败" ); return -1 ; } socklen_t addrlen = sizeof (struct sockaddr_in); while (1 ) { struct client * client = NULL ; for (i = 0 ; i < NR_CLIENTS; i++) { if (clients[i].connectionSocket == UNCONNECTED) { client = &clients[i]; break ; } } if (!client) continue ; int socket = accept(welcomeSocket, (struct sockaddr*)&client->address, &addrlen); client->connectionSocket = socket; if (socket == -1 ) { perror("与客户端建立连接失败" ); continue ; } pthread_t tid; pthread_create(&tid, NULL , thread, client); pthread_detach(tid); } close(welcomeSocket); return 0 ; }
IO多路复用 有三种方式:select、poll、epoll
其中只有select是可以跨平台的,poll和epoll只支持Linux
select和poll底层实现是线性表,epoll底层实现是红黑树。从效率上来看,epoll的事件驱动效率会高于其它两个
select有上限限制,默认 1024。其它两个没有限制
用select实现IO多路复用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <sys/select.h> struct timeval { time_t tv_sec; suseconds_t tv_usec; }; int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval * timeout) ;void FD_CLR (int fd, fd_set *set ) ;int FD_ISSET (int fd, fd_set *set ) ;void FD_SET (int fd, fd_set *set ) ;void FD_ZERO (fd_set *set ) ;
服务器代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/select.h> #define NR_CLIENTS 128 #define BUFSIZE 1024 #define MSGSIZE 128 #define NFDS(fd) (fd+1) int main () { const unsigned short int PORT = 8080 ; int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("lfd创建失败" ); return -1 ; } struct sockaddr_in addr ; memset (&addr, 0 , sizeof (addr)); addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); int res; res = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (res == -1 ) { perror("lfd绑定失败" ); return -1 ; } listen(lfd, NR_CLIENTS); if (res == -1 ) { perror("监听失败" ); return -1 ; } fd_set read_set; FD_ZERO(&read_set); FD_SET(lfd, &read_set); fd_set write_set; fd_set except_set; struct timeval time ; time.tv_sec = 0 ; time.tv_usec = 0 ; struct timeval * timeval = NULL ; int mfd = lfd; while (1 ) { fd_set temp_set = read_set; int res = select(NFDS(mfd), &temp_set, &write_set, &except_set, timeval); struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad); memset (&cad, 0 , sizeof (cad)); char cip[32 ]; unsigned short int cport; if (FD_ISSET(lfd, &temp_set)) { int cfd = accept(lfd, (struct sockaddr*)&cad, &cadlen); FD_SET(cfd, &read_set); mfd = cfd > mfd ? cfd : mfd; inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); printf ("与客户端[%s:%d]连接成功\n" , cip, cport); } for (int fd = 0 ; fd < NFDS(mfd); fd++) { if (fd != lfd && FD_ISSET(fd, &temp_set)) { unsigned char buf[BUFSIZE]; memset (buf, 0 , sizeof (buf)); res = getpeername(fd, (struct sockaddr*)&cad, &cadlen); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "fd:%d获取客户端地址失败" , fd); perror(error); continue ; } inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); int len = recv(fd, buf, sizeof (buf) - 1 , 0 ); if (len == -1 ) { char error[MSGSIZE]; sprintf (error, "与客户端[%s:%d]接收数据失败" , cip, cport); perror(error); continue ; } else if (len == 0 ) { char message[MSGSIZE]; sprintf (message, "与客户端[%s:%d]断开连接\n" , cip, cport); printf (message); FD_CLR(fd, &read_set); close(fd); continue ; } printf ("客户端[%s:%d]: %s" , cip, cport, buf); for (int i = 0 ; i < len; i++) { buf[i] &= 0xdf ; } res = send(fd, buf, len, 0 ); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "向客户端[%s:%d]发送数据失败" , cip, cport); perror(error); continue ; } } } } close (lfd); return 0 ; }
需要注意的是,select函数是将传入参数当传出参数来用,也就是说它会修改原来的参数。read_set是我们用来记录有效fd的所以不能被传进去轻易的被修改,所以需要创建一个它的拷贝副本temp_set传进去,然后从这个临时变量中读取想要的数据。
用select实现的IO多路复用 核心思想就是操作系统提供一个函数给你,你把要遍历的文件描述符统一交给操作系统,操作系统会统一检查这些文件描述符的缓冲区里有没有数据(这里以读缓冲区为例),再把缓冲区中有数据的文件描述符筛选出来返回给你。这样我们只需要遍历这些确定有数据的文件描述符调用read()来读取数据就可以了,不需要像原来一样把所有文件描述都调用一遍read(),造成当前线程的在某个文件描述符上堵塞,以及避免一些切换到内核区所需要的资源开销的浪费。
IO多路转接(复用)之select | 爱编程的大丙 (subingwen.cn)
poll和select的原理差不多,都是由操作系统统一检查文件描述符的缓冲区,返回就绪的文件描述符,避免用户调用read()造成的当前进程的阻塞和切换到内核去的资源开销浪费。
但是和select不同的是,select操作的是位图,而poll会针对每一个pollfd数组内元素预留的events来根据文件描述符读写缓冲区的实际情况设置对应的revents来表示函数返回的结果。
出于跨平台和效率的考虑,一般不会选择poll。
1 2 3 4 5 6 7 8 9 10 11 12 #include <poll.h> struct pollfd { int fd; short events; short revents; }; struct pollfd myfd [100];int poll (struct pollfd *fds, nfds_t nfds, int timeout) ;
IO多路转接(复用)之poll | 爱编程的大丙 (subingwen.cn)
epoll(eventpoll)
1 int epoll_create (int size) ;
这个函数会创建一个epoll树或者说一个epoll实例,因为一个进程中可以不止创建一个epoll实例,所以这个函数会返回一个整数用于标识此次创建的epoll实例。这个整数其实就是一个文件描述符,这个标识某个epoll实例的文件描述符也会在当前进程的文件描述符表中占据一个位置。所以在不使用某个epoll实例后需要close(epfd)来归还文件描述符,释放资源。
函数参数 size:在Linux内核2.6.8版本以后,这个参数是被忽略的,只需要指定一个大于0的数值就可以了。
函数返回值:
失败:返回-1
成功:返回一个有效的文件描述符,通过这个文件描述符就可以访问创建的epoll实例了
epoll的实例本质上是一颗红黑树。树节点的具体实现是struct epitem,源码有些复杂
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct epitem { union { struct rb_node rbn ; struct rcu_head rcu ; }; struct list_head llink ; struct epitem *next ; struct epoll_filefd ffd ; struct eventpoll *ep ; struct list_head rdllink ; struct epoll_event event ; u64 data; u64 data1; };
但是可供用户操作成员变量就是fd和event。或者说这个结构体的重点就是这两个,干脆抽象成这两个好了。
1 2 3 4 5 +--------------+ | fd | +--------------+ | event | +--------------+
fd,是文件描述符,也就是个整数,没什么好说的,event的具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct epoll_event { uint32_t events; epoll_data_t data; }; typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ;
epoll_ctl()函数的作用是管理红黑树实例上的节点,可以进行添加、删除、修改操作。
1 int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ;
函数参数:
epfd:epoll_create() 函数的返回值,通过这个参数找到epoll实例
op:这是一个枚举值,控制通过该函数执行什么操作
EPOLL_CTL_ADD:往epoll模型中添加新的节点
EPOLL_CTL_MOD:修改epoll模型中已经存在的节点
EPOLL_CTL_DEL:删除epoll模型中的指定的节点
fd:文件描述符,即要添加/修改/删除的文件描述符
event:epoll事件,用来修饰第三个参数对应的文件描述符的,指定检测这个文件描述符的什么事件
events:委托epoll检测的事件
EPOLLIN:读事件, 接收数据, 检测读缓冲区,如果有数据该文件描述符就绪
EPOLLOUT:写事件, 发送数据, 检测写缓冲区,如果可写该文件描述符就绪
EPOLLERR:异常事件
data:用户数据变量,这是一个联合体类型,通常情况下使用里边的fd成员,用于存储待检测的文件描述符的值,在调用epoll_wait()函数的时候这个值会被传出。
函数返回值: 失败:返回-1 成功:返回0
需要注意的是,用epoll_ctl()函数做添加操作的时候,虽然传进去的事件是以指针的方式传进去的,但是真正创建节点,设置节点的event时采用的是拷贝的方式,源码是下面这样。
所以会看到下面的代码只会定义一个event当作公共变量。
1 int epoll_wait (int epfd, struct epoll_event * events, int maxevents, int timeout) ;
函数参数:
epfd:epoll_create() 函数的返回值, 通过这个参数找到epoll实例
events:传出参数, 这是一个结构体数组的地址, 里边存储了已就绪的文件描述符的信息
maxevents:修饰第二个参数, 结构体数组的容量(元素个数)
timeout:如果检测的epoll实例中没有已就绪的文件描述符,该函数阻塞的时长, 单位ms 毫秒
0:函数不阻塞,不管epoll实例中有没有就绪的文件描述符,函数被调用后都直接返回
大于0:如果epoll实例中没有已就绪的文件描述符,函数阻塞对应的毫秒数再返回
-1:函数一直阻塞,直到epoll实例中有已就绪的文件描述符之后才解除阻塞
函数返回值:
成功:
等于0:函数是阻塞被强制解除了, 没有检测到满足条件的文件描述符
大于0:检测到的已就绪的文件描述符的总个数
失败:返回-1
IO多路转接(复用)之epoll | 爱编程的大丙 (subingwen.cn)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/epoll.h> #define NR_CLIENTS 128 #define EPOLL 1 #define EVSSIZE 1024 #define BUFSIZE 1024 #define MSGSIZE 128 int main () { const unsigned short int PORT = 8080 ; struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); int res; int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("lfd创建失败" ); return -1 ; } res = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (res == -1 ) { perror("lfd绑定失败" ); return -1 ; } res = listen(lfd, NR_CLIENTS); if (res == -1 ) { perror("lfd监听失败" ); return -1 ; } int epoll = epoll_create(EPOLL); if (epoll == -1 ) { perror("epoll创建失败" ); return -1 ; } struct epoll_event event ; struct epoll_event events [EVSSIZE ]; struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad); char cip[32 ]; unsigned short int cport; event.events = EPOLLIN; event.data.fd = lfd; epoll_ctl(epoll, EPOLL_CTL_ADD, lfd, &event); while (1 ) { int event_count = epoll_wait(epoll, events, EVSSIZE, -1 ); for (int i = 0 ; i < event_count; i++) { int fd = events[i].data.fd; if (fd == lfd) { int cfd = accept(lfd, (struct sockaddr*)&cad, &cadlen); event.events = EPOLLIN; event.data.fd = cfd; epoll_ctl(epoll, EPOLL_CTL_ADD, cfd, &event); inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); printf ("与客户端[%s:%d]连接成功\n" , cip, cport); } else { unsigned char buf[BUFSIZE]; memset (buf, 0 , sizeof (buf)); res = getpeername(fd, (struct sockaddr*)&cad, &cadlen); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "fd:%d获取客户端地址失败" , fd); perror(error); continue ; } inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); int len = recv(fd, buf, sizeof (buf) - 1 , 0 ); if (len == -1 ) { char error[MSGSIZE]; sprintf (error, "与客户端[%s:%d]接收数据失败" , cip, cport); perror(error); continue ; } else if (len == 0 ) { char message[MSGSIZE]; sprintf (message, "与客户端[%s:%d]断开连接\n" , cip, cport); printf (message); epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL ); close(fd); continue ; } printf ("客户端[%s:%d]: %s" , cip, cport, buf); for (int i = 0 ; i < len; i++) { buf[i] &= 0xdf ; } res = send(fd, buf, len, 0 ); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "向客户端[%s:%d]发送数据失败" , cip, cport); perror(error); continue ; } } } } close(epoll); close(lfd); return 0 ; }
触发模式是针对epoll树里的某个节点的,或者说某个节点的event
水平触发模式:代码里的buf就这么大,每次读取不能将文件描述符的读缓冲区里数据全部读完,这样在下次循环检测的时候,这个文件描述符的读缓冲区仍然会被标记为就绪状态,且仍然从上次没读取完的数据开始读取。对于写缓冲区来说,只要写缓冲区不是满的就是就绪状态,epoll每次都会发送通知。
边沿触发模式:代码里的buf就这么大,如果这次没有将读缓冲区里的数据全部读完,在下次循环中epoll不会再把这个缓冲区标记为就绪状态,也不会返回通知,只有当下一条数据从客户端接收到,这个读缓冲区才会被标记为就绪状态。对于写缓冲区来说,一开始空的,epoll标记为就绪状态,只返回这一次通知,后面即使写缓冲区没满也不会通知,下次通知只有在写缓冲区从满到不满,写缓冲区才会被标记为就绪返回通知。
对于读缓冲区来说:
水平触发模式(默认):只要缓冲区内数据没有读完,就会被标记为就绪,在下次检测时会返回通知
边沿触发模式:只有当新的数据到达时,才会被标记为就绪,在下次检测时返回通知
对于写缓冲区来说:
水平触发模式(默认):只要缓冲区内还没有写满,就会被标记为就绪,在下次检测时会返回通知
边沿触发模式:只有当缓冲区从满到不满,或者一开始就是不满时才会被标记为就绪,在下次检测时返回通知
recv()函数能否调用,能否读取内核中读缓冲区的数据,这与epoll或者说IO多路复用的返回值无关,真正能决定recv()函数还能不能继续读下去的应该是缓冲区本身有记录的接收到的字节数这个成员变量。但是每次调用recv()函数的开销是很大的,什么时候调用,而让recv()每次都能读取到数据,不会跑空,不会因此而阻塞,换句话说我们在调用recv()函数前就需要知道读缓冲区的情况才能更好的决定是否要调用recv()函数。内核提供的IO多路复用就是为此而存在的,像select()、epoll_wait()这样的函数,就能够让内核在内核态中检测各个读缓冲区的情况,再返回通知告诉我们哪些socket的缓冲区是有数据的,让我们可以挑这些有数据的缓冲区去高效地调用recv()。而让内核决定是否对某个缓冲区返回通知,这就是两种触发模式地区别。
边沿触发模式代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 #include <stdio.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <errno.h> #include <arpa/inet.h> #include <sys/epoll.h> #define NR_CLIENTS 128 #define EPOLL 1 #define EVSSIZE 1024 #define BUFSIZE 5 #define MSGSIZE 128 int main () { const unsigned short int PORT = 8080 ; struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); int res; int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("lfd创建失败" ); return -1 ; } res = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (res == -1 ) { perror("lfd绑定失败" ); return -1 ; } res = listen(lfd, NR_CLIENTS); if (res == -1 ) { perror("lfd监听失败" ); return -1 ; } int epoll = epoll_create(EPOLL); if (epoll == -1 ) { perror("epoll创建失败" ); return -1 ; } struct epoll_event event ; struct epoll_event events [EVSSIZE ]; struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad); char cip[32 ]; unsigned short int cport; event.events = EPOLLIN | EPOLLET; event.data.fd = lfd; epoll_ctl(epoll, EPOLL_CTL_ADD, lfd, &event); while (1 ) { int event_count = epoll_wait(epoll, events, EVSSIZE, -1 ); for (int i = 0 ; i < event_count; i++) { int fd = events[i].data.fd; if (fd == lfd) { int cfd = accept(lfd, (struct sockaddr*)&cad, &cadlen); int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); event.events = EPOLLIN | EPOLLET; event.data.fd = cfd; epoll_ctl(epoll, EPOLL_CTL_ADD, cfd, &event); inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); printf ("与客户端[%s:%d]连接成功\n" , cip, cport); } else { unsigned char buf[BUFSIZE]; res = getpeername(fd, (struct sockaddr*)&cad, &cadlen); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "fd:%d获取客户端地址失败" , fd); perror(error); continue ; } inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); while (1 ) { int len = recv(fd, buf, sizeof (buf)-1 , 0 ); if (len == -1 ) { char error[MSGSIZE]; if (errno == EAGAIN) { printf ("数据接收完毕...\n" ); } break ; } else if (len == 0 ) { char message[MSGSIZE]; sprintf (message, "与客户端[%s:%d]断开连接\n" , cip, cport); printf (message); epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL ); close(fd); continue ; } printf ("客户端[%s:%d]: %s" , cip, cport, buf); for (int i = 0 ; i < len; i++) { buf[i] &= 0xdf ; } res = send(fd, buf, len, 0 ); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "向客户端[%s:%d]发送数据失败" , cip, cport); perror(error); continue ; } } } } } close(epoll); close(lfd); return 0 ; }
基于边沿触发模式的多线程代码
每次有新连接时会创建一个线程,建立连接后线程马上就会销毁
在已经建立的连接上,如果客户端发来数据,就会创建一个线程,处理完数据后线程马上销毁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <fcntl.h> #include <errno.h> #include <pthread.h> #include <arpa/inet.h> #include <sys/epoll.h> #define NR_CLIENTS 128 #define EPOLL 1 #define EVSSIZE 1024 #define BUFSIZE 5 #define MSGSIZE 128 #define DATASIZE 1024 typedef struct _accept_t accept_t ;struct _accept_t { int lfd; int epoll; }; void * acceptance (void * arg) { accept_t * args = (accept_t *)arg; int lfd = args->lfd; int epoll = args->epoll; struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad); char cip[32 ]; unsigned short int cport; struct epoll_event event ; int cfd = accept(lfd, (struct sockaddr*)&cad, &cadlen); int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); event.events = EPOLLIN | EPOLLET; event.data.fd = cfd; epoll_ctl(epoll, EPOLL_CTL_ADD, cfd, &event); inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); printf ("与客户端[%s:%d]连接成功\n" , cip, cport); free (args); return NULL ; } typedef struct _commu_t commu_t ;struct _commu_t { int fd; int epoll; }; void * communication (void * arg) { commu_t * args = (commu_t *)arg; int fd = args->fd; int epoll = args->epoll; struct sockaddr_in cad ; socklen_t cadlen = sizeof (cad); char cip[32 ]; unsigned short int cport; int res = getpeername(fd, (struct sockaddr*)&cad, &cadlen); if (res == -1 ) { char error[MSGSIZE]; sprintf (error, "fd:%d获取客户端地址失败" , fd); perror(error); pthread_exit(NULL ); } inet_ntop(AF_INET, &cad.sin_addr.s_addr, cip, sizeof (cip)); cport = ntohs(cad.sin_port); unsigned char buf[BUFSIZE]; unsigned char data[DATASIZE]; memset (data, 0 , sizeof (data)); while (1 ) { memset (buf, 0 , sizeof (buf)); int len = recv(fd, buf, sizeof (buf)-1 , 0 ); if (len == -1 ) { char error[MSGSIZE]; if (errno == EAGAIN) { printf ("数据接收完毕...\n" ); res = send(fd, data, strlen (data) + 1 , 0 ); if (res == -1 ) { sprintf (error, "向客户端[%s:%d]发送数据失败" , cip, cport); perror(error); pthread_exit(NULL ); } } break ; } else if (len == 0 ) { char message[MSGSIZE]; sprintf (message, "与客户端[%s:%d]断开连接\n" , cip, cport); printf (message); epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL ); close(fd); break ; } printf ("客户端[%s:%d]: %s" , cip, cport, buf); for (int i = 0 ; i < len; i++) { buf[i] &= 0xdf ; } strncat (data+strlen (data), buf, len); } free (args); return NULL ; } int main () { const unsigned short int PORT = 8080 ; struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); int res; int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("lfd创建失败" ); return -1 ; } res = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (res == -1 ) { perror("lfd绑定失败" ); return -1 ; } res = listen(lfd, NR_CLIENTS); if (res == -1 ) { perror("lfd监听失败" ); return -1 ; } int epoll = epoll_create(EPOLL); if (epoll == -1 ) { perror("epoll创建失败" ); return -1 ; } struct epoll_event event ; struct epoll_event events [EVSSIZE ]; event.events = EPOLLIN | EPOLLET; event.data.fd = lfd; epoll_ctl(epoll, EPOLL_CTL_ADD, lfd, &event); while (1 ) { int event_count = epoll_wait(epoll, events, EVSSIZE, -1 ); for (int i = 0 ; i < event_count; i++) { int fd = events[i].data.fd; pthread_t tid; if (fd == lfd) { accept_t * arg = (accept_t *)malloc (sizeof (accept_t )); arg->lfd = lfd; arg->epoll = epoll; pthread_create(&tid, NULL , acceptance, arg); pthread_detach(tid); } else { commu_t * arg = (commu_t *)malloc (sizeof (commu_t )); arg->fd = fd; arg->epoll = epoll; pthread_create(&tid, NULL , communication, arg); pthread_detach(tid); } } } close(epoll); close(lfd); return 0 ; }
在 Windows 和 Linux 下进行套接字编程时,所需的头文件有所不同。以下是对比分析:
1. Windows 下的头文件 Windows 使用的是 Winsock(Windows Sockets)API,所需头文件如下:
必要的头文件
<winsock2.h> 提供基本的套接字函数和数据结构,例如 socket()、bind()、listen()、connect()、recv()、send()、closesocket() 等。
<ws2tcpip.h> 提供对现代网络协议(如 IPv6)和功能(如 getaddrinfo()、inet_ntop())的支持。
额外头文件
<windows.h> 如果程序涉及到其他 Windows API 功能(如多线程、事件等),可能需要包含。
库文件
需要链接 Ws2_32.lib,可以通过 #pragma comment(lib, "Ws2_32.lib") 指令自动链接,或在项目设置中手动添加。
2. Linux 下的头文件 Linux 使用的是 POSIX 套接字 API,头文件更简洁:
必要的头文件
<arpa/inet.h> 提供 IP 地址的转换函数,例如 inet_pton() 和 inet_ntop()。还包含了<sys/socket.h>和<netinet/in.h>。这个头文件可能是基于下面这两个头文件制作的,所以包含了这个头文件就不需要包含下面两个头文件了。
<sys/socket.h> 提供套接字函数和数据结构,例如 socket()、bind()、listen()、accept()、connect() 等。
<netinet/in.h> 提供与网络地址相关的结构(如 sockaddr_in)和常量(如 INADDR_ANY、htons())。
<unistd.h> 提供 POSIX 接口的常规函数,例如 close(),用于关闭套接字。
额外头文件
<netdb.h> 提供主机名解析的函数,例如 getaddrinfo() 和 gethostbyname()。
Windows 和 Linux 的头文件对比表
功能
Windows 头文件
Linux 头文件
基本套接字函数
<winsock2.h>
<sys/socket.h>
地址族、协议、常量定义
<winsock2.h>
<netinet/in.h>
地址转换函数
<ws2tcpip.h>
<arpa/inet.h>
主机名解析
<ws2tcpip.h>
<netdb.h>
关闭套接字函数
<winsock2.h>
<unistd.h>
系统通用功能
<windows.h>
<unistd.h> (POSIX)
3. 使用差异 1) closesocket() 和 close()
Windows 下:套接字关闭需要用 closesocket(),因为套接字是特定于 Winsock 的资源。
Linux 下:直接使用 close() 函数,因为套接字被视为文件描述符。
2) 初始化与清理
Windows:
必须在使用任何网络功能前调用 WSAStartup() 初始化 Winsock。
使用完套接字后调用 WSACleanup() 进行清理。
Linux:
不需要专门的初始化或清理操作,直接使用套接字 API。
3) 错误处理
Windows :使用 WSAGetLastError() 获取错误码。
Linux :错误通过 errno 提供,使用 perror() 或 strerror() 打印。
4. 示例对比 以下是 Windows 和 Linux 下一个简单的 TCP 客户端的代码示例,展示如何使用这些头文件。
Windows 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #pragma comment(lib, "Ws2_32.lib" ) int main () { WSADATA wsaData; SOCKET sock; struct sockaddr_in server ; if (WSAStartup(MAKEWORD(2 , 2 ), &wsaData) != 0 ) { printf ("WSAStartup failed\n" ); return 1 ; } sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (sock == INVALID_SOCKET) { printf ("Socket creation failed: %d\n" , WSAGetLastError()); WSACleanup(); return 1 ; } server.sin_family = AF_INET; server.sin_port = htons(80 ); inet_pton(AF_INET, "93.184.216.34" , &server.sin_addr); if (connect(sock, (struct sockaddr *)&server, sizeof (server)) == SOCKET_ERROR) { printf ("Connection failed: %d\n" , WSAGetLastError()); closesocket(sock); WSACleanup(); return 1 ; } printf ("Connected successfully\n" ); closesocket(sock); WSACleanup(); return 0 ; }
Linux 示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <arpa/inet.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main () { int sock; struct sockaddr_in server ; sock = socket(AF_INET, SOCK_STREAM, 0 ); if (sock == -1 ) { perror("Socket creation failed" ); return 1 ; } server.sin_family = AF_INET; server.sin_port = htons(80 ); inet_pton(AF_INET, "93.184.216.34" , &server.sin_addr); if (connect(sock, (struct sockaddr *)&server, sizeof (server)) == -1 ) { perror("Connection failed" ); close(sock); return 1 ; } printf ("Connected successfully\n" ); close(sock); return 0 ; }
5. 总结
特性
Windows
Linux
头文件
<winsock2.h>, <ws2tcpip.h>
<arpa/inet.h>
库文件
必须链接 Ws2_32.lib
无需额外库
关闭套接字
closesocket()
close()
初始化
必须调用 WSAStartup()
无需初始化
错误处理
使用 WSAGetLastError()
使用 errno
1 2 3 4 5 6 7 8 9 10 struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; struct sockaddr *ai_addr ; char *ai_canonname; struct addrinfo *ai_next ; };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "Ws2_32.lib" ) int main () { WSADATA wsaData; int result; char ipstr[INET6_ADDRSTRLEN]; result = WSAStartup(MAKEWORD(2 , 2 ), &wsaData); if (result != 0 ) { printf ("WSAStartup failed: %d\n" , result); return 1 ; } const char *hostname = "36.155.132.76" ; const char *service = NULL ; struct addrinfo hints , *res , *p ; memset (&hints, 0 , sizeof (hints)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; result = getaddrinfo(hostname, service, &hints, &res); if (result != 0 ) { printf ("getaddrinfo failed: %s\n" , gai_strerror(result)); WSACleanup(); return 1 ; } printf ("IP addresses for www.baidu.com:\n" ); for (p = res; p != NULL ; p = p->ai_next) { void *addr; char *ipver; if (p->ai_family == AF_INET) { struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); ipver = "IPv4" ; } else { struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); ipver = "IPv6" ; } inet_ntop(p->ai_family, addr, ipstr, sizeof (ipstr)); printf (" %s: %s\n" , ipver, ipstr); } freeaddrinfo(res); WSACleanup(); return 0 ; }