1.socket 通信 1.1 大小端转换
主机字节序 16 位值 <==> 网络字节序 16 位值
主机字节序 32 位值 <==> 网络字节序 32 位值
1 2 3 4 5 6 7 8 9 #include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; unit32_t htonl (unit32_t hostlong) ; uint16_t ntohs (uint16_t netshort) ;unit32_t ntohl (unit32_t netlong) ;
1.2 IP地址转换
主机字节序的字符串IP地址 <==> 网络字节序的整形IP地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <arpa/inet.h> int inet_pton (int af, const char * src, void * dst) ;const char *int_ntop (int af, const void *src, char *dst, socklen_t size) ;
1.3 套接字相关函数 1.3.1 socket 创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <arpa/inet.h> int socket (int domain, int type, int protocol) ;
1.3.2 bind 绑定套接字 将监听的套接字和本地IP和端口进行关联
1 2 3 4 5 6 7 8 9 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
1.3.3 listen 监听套接字 给监听的套接字设置监听,开始检测客户端链接
1 2 3 4 5 6 int listen (int sockfd, int backlog) ;
1.3.4 accept 接收客户端连接 等待并接受客户端的连接,阻塞函数,没有客户端连接就阻塞,监听的文件描述符缓冲区没有数据就阻塞,有数据就解除阻塞建立连接,连接建立成功后,返回一个通信用的文件描述符
1 2 3 4 5 6 7 8 9 int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
1.3.5 read、recv 读数据 读取数据,如果数据区空会读堵塞
1 2 3 4 5 6 7 8 9 10 11 12 13 ssize_t read (int sockfd, void *buf, size_t size) ;ssize_t recv (int sockfd, void *buf, size_t size, int flags) ;
1.3.6 write、send 写数据 发送数据,如果数据区满会写阻塞
1 2 3 4 5 6 7 ssize_t write (int fd, const void *buf, size_t len) ;ssize_t send (int fd, const void *buf, size_t len, int flags) ;
1.3.7 recvfrom / sendto 发送接收
1 2 3 4 5 6 7 8 9 10 ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) ;
1 2 3 4 5 6 7 8 9 10 ssize_t sendto (int sockfd, void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t *addrlen) ;
1.3.8 connect 客户端连接 客户端连接服务器
1 2 3 4 5 6 7 int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
1.4 套接字选项 该函数用来设置套接字选项,端口复用、广播、组播等,下面是端口复用的参数解释
1 2 3 4 5 6 7 8 9 10 11 int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ;
2. IO多路复用 2.1 select
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <sys/select.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ;
timeval 结构体
1 2 3 4 struct timeval { time_t tv-sec; suseconds_t tv_usec; };
fd_set 文件描述符集合(位操作)操作函数
1 2 3 4 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 ) ;
2.2 epoll 在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。
epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:
1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字
3)调用epoll_wait收集发生的事件的连接
如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
2.2.1 epoll_create 创建 epoll 1 2 3 4 5 6 7 8 #include <sys/epoll.h> int epoll_create (int size) ;
2.2.2 epoll_ctl 操作epoll 实现对 epoll 树上节点的操作(添加、修改、删除节点)
1 2 3 4 5 6 7 8 9 10 11 12 int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ;
1 2 3 4 5 6 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ;
epoll_event
event 是位操作,EPOLLIN 检测写缓冲区,EPOLLOUT 检测读缓冲区
data.fd 等于 epoll_ctl 函数调用的第三个参数
1 2 3 4 struct epoll_event { uint32_t event; epoll_data_t data; };
2.2.3 epoll_wait 阻塞函数,委托内核检测epoll树上文件描述符的状态,如果没有状态变化,默认一直阻塞
1 2 3 4 5 6 7 8 int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout) ;
2.2.4 Level triggered 水平模式(默认) LT(level triggered)是缺省的工作方式,同时支持 block 和 no-block socket。这种模式下,内核会通知文件描述符是否就绪,如果不进行任何操作,内核会一直通知你该文件描述符就绪
2.2.5 Edge triggered 边沿模式 ET(edge triggered)是高速工作模式,只支持 no-block socket。这种模式下,如果接到通知,但是没有把数据从缓冲区读完,epoll_wait不会再次通知;直到再次接收到新数据也一样通知一次,但是此时他会接着上次的缓冲区数据读。
1 2 3 struct epoll_event ev ;ev.events = EPOLLIN | EPOLLET; ev.data.fd = lfd;
使用边沿模式读数据需要在收到消息后我们一般需要 while(1) 死循环读取数据直到缓冲区数据读完,所以需要设置文件描述符为非阻塞状态,让read可以非阻塞读取数据,通过 read 的返回值判断是否结束该死循环
1 2 3 4 5 int fcntl (int fd, int cmd, ...) ;int flag = fcntl(cfd, F_GETFL);flag = flag | O_NONBLOCK; fcntl(cfd, F_SETFL, flag);
最后因为这里已经设置为非阻塞,可以根据read的返回值判断是否已经读完缓冲区了,如果读完了会有errno EAGAIN的错误码,根据该错误码跳出循环即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 while (1 ){ int len = recv(curfd, buf, sizeof (buf), 0 ); if (len > 0 ) printf ("打印接收的数据" ); else if ( len == 0 ) printf ("断开连接" ); else { if (errno==EAGAIN) { printf ("数据读完了" ); break ; } perror("接收错误" ); exit (0 ); } }
3. 代码示例 3.1 TCP、epoll服务器
创建socket套接字
绑定ip和端口
设置监听
初始化一个epoll树
将文件描述符加入epoll树
委托内核检测文件描述符状态
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/epoll.h> int main () { int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("socket error" ); exit (1 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_addr.s_addr= INADDR_ANY; addr.sin_port = htons(8989 ); int ret = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (ret == -1 ) { perror("bind error" ); exit (2 ); } ret = listen(lfd, 128 ); if (ret == -1 ) { perror("listen error" ); exit (3 ); } int epfd = epoll_create(1 ); if (epfd == -1 ) { perror("epoll_create error" ); exit (4 ); } struct epoll_event ev ; ev.events = EPOLLIN; ev.data.fd = lfd; ret = epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev); if (ret == -1 ) { perror("epoll_ctl" ); exit (5 ); } struct epoll_event evs [1024]; int size = sizeof (evs) / sizeof (evs[0 ]); while (1 ) { int num = epoll_wait(epfd, evs, size, -1 ); printf ("num = %d\n" , num); for (int i=0 ; i<num; i++) { int curfd = evs[i].data.fd; if (curfd == lfd) { int cfd = accept(lfd, NULL , NULL ); ev.events = EPOLLIN; ev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev); } else { char buf[1024 ]; memset (buf, 0 , sizeof (buf)); int len = recv(curfd, buf, sizeof (buf), 0 ); if (len == 0 ) { printf ("客户端断开了链接...\n" ); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL ); close(curfd); } else if (len>0 ) { printf ("recv data: %s\n" ); send(curfd, buf, len, 0 ); } else { perror("recv error" ); exit (6 ); } } } } }
3.2 UDP 3.2.1 服务器
UDP服务器需要创建套接字
绑定端口
接收数据
根据接收数据的客户端发送数据
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(8989 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof (addr)); if (ret==-1 ) { perror("bind" ); exit (0 ); } char ip[24 ]; char buf[1024 ]; struct sockaddr_in cliaddr ; int clilen = sizeof (cliaddr); while (1 ) { int len = recvfrom(fd, buf, sizeof (buf), 0 , (struct sockaddr*)&cliaddr, &clilen); if (len==-1 ) { break ; } printf ("client ip: %s, port: %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof (ip)), ntohs(cliaddr.sin_port)); printf ("client say: %s\n" , buf); sendto(fd, buf, strlen (buf)+1 , 0 , (struct sockaddr*)&cliaddr, clilen); } close(fd); return 0 ; }
3.2.2 客户端
UDP客户端相对于服务器端减少了手动绑定ip端口的步骤
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in serveraddr ; serveraddr.sin_family = AF_INET; serveraddr.sin_port = htons(8989 ); inet_pton(AF_INET, "10.0.2.15" , &serveraddr.sin_addr.s_addr); char ip[24 ]; char buf[1024 ]; int num=0 ; while (1 ) { sprintf (buf, "Hello World!, %d\n" , num++); sendto(fd, buf, strlen (buf)+1 , 0 , (struct sockaddr*)&serveraddr, sizeof (serveraddr)); memset (buf, 0 , sizeof (buf)); int len = recvfrom(fd, buf, sizeof (buf), 0 , NULL , NULL ); if (len==-1 ) { break ; } printf ("client say: %s\n" , buf); } close(fd); return 0 ; }
3.3 UDP广播 3.3.1 服务器
服务器创建socket
设置广播属性
广播开销很小 ,只使用广播地址就可以发送到多个接收端
但只能在局域网 内使用
发送端要设置广播属性,将消息发送到广播地址和端口,接收端在对应的端口等待
向广播ip端发送数据
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } int opt = 1 ; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &opt, sizeof (opt)); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(8989 ); inet_pton(AF_INET, "10.0.2.255" , &cliaddr.sin_addr.s_addr); char buf[1024 ]; int num = 0 ; while (1 ) { sprintf (buf, "发送广播数据: %d\n" , num++); sendto(fd, buf, strlen (buf)+1 , 0 , (struct sockaddr*)&cliaddr, sizeof (cliaddr)); printf ("%s\n" , buf); sleep(1 ); } close(fd); return 0 ; }
3.3.2 客户端
客户端创建socket
绑定固定的端口用来接收数据
recvfrom接收数据
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(8989 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof (addr)); if (ret==-1 ) { perror("bind" ); exit (0 ); } char ip[24 ]; char buf[1024 ]; while (1 ) { memset (buf, 0 , sizeof (buf)); int len = recvfrom(fd, buf, sizeof (buf), 0 , NULL , NULL ); if (len==-1 ) { break ; } printf ("boardcast say: %s\n" , buf); } close(fd); return 0 ; }
3.4 UDP组播
组播只需要发送到特定地址,发送端开销很小
组播需要组播地址,一种是Internet中使用,另一种是局域网使用,需要加入到多播组
相对于广播,组播支持广域网
3.4.1 服务器 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } struct in_addr addr ; inet_pton(AF_INET, "239.0.0.10" , &addr.s_addr); setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &addr, sizeof (addr)); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(10000 ); inet_pton(AF_INET, "239.0.0.10" , &cliaddr.sin_addr.s_addr); char buf[1024 ]; int num = 0 ; while (1 ) { sprintf (buf, "组播数据: %d\n" , num++); sendto(fd, buf, strlen (buf)+1 , 0 , (struct sockaddr*)&cliaddr, sizeof (cliaddr)); printf ("%s\n" , buf); sleep(1 ); } close(fd); return 0 ; }
3.4.2 客户端 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> int main () { int fd = socket(AF_INET, SOCK_DGRAM, 0 ); if (fd==-1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(10000 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr*)&addr, sizeof (addr)); if (ret==-1 ) { perror("bind" ); exit (0 ); } struct ip_mreqn op ; op.imr_address.s_addr = INADDR_ANY; inet_pton(AF_INET, "239.0.0.10" , &op.imr_multiaddr.s_addr); op.imr_ifindex = if_nametoindex("ens33" ); setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof (op)); char ip[24 ]; char buf[1024 ]; while (1 ) { memset (buf, 0 , sizeof (buf)); int len = recvfrom(fd, buf, sizeof (buf), 0 , NULL , NULL ); if (len==-1 ) { break ; } printf ("boardcast say: %s\n" , buf); } close(fd); return 0 ; }
3.5 本地套接字用于进程间通信 3.5.1 服务器端 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/un.h> int main () { int fd = socket(AF_LOCAL, SOCK_STREAM, 0 ); struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "./server.sock" ); int ret = bind(fd, (struct sockaddr*)&addr, sizeof (addr)); printf ("HELLO\n" ); listen(fd, 128 ); struct sockaddr_un cliaddr ; int clilen = sizeof (cliaddr); int cfd = accept(fd, (struct sockaddr*)&cliaddr, &clilen); printf ("客户端套接字文件路径和名字:%s\n" , cliaddr.sun_path); while (1 ) { char buf[1024 ]; memset (buf, 0 , sizeof (buf)); int len = recv(cfd, buf, sizeof (buf), 0 ); if (len==0 ) { printf ("客户端断开连接\n" ); break ; } else if (len>0 ) { printf ("client say: %s\n" , buf); send(cfd, buf, len, 0 ); } else { perror("recv" ); break ; } } close(fd); return 0 ; }
3.5.2客户端 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <sys/un.h> int main () { int fd = socket(AF_LOCAL, SOCK_STREAM, 0 ); struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "./client.sock" ); int ret = bind(fd, (struct sockaddr*)&addr, sizeof (addr)); struct sockaddr_un seraddr ; seraddr.sun_family = AF_LOCAL; strcpy (seraddr.sun_path, "./server.sock" ); ret = connect(fd, (struct sockaddr*)&seraddr, sizeof (seraddr)); int num = 0 ; while (1 ) { char buf[1024 ]; sprintf (buf, "本地套接字通信, %d\n" , num++); send(fd, buf, strlen (buf)+1 , 0 ); memset (buf, 0 , sizeof (buf)); recv(fd, buf, sizeof (buf), 0 ); printf ("server say: %s\n" , buf); } close(fd); return 0 ; }
Author:
mxwu
Permalink:
https://mingxuanwu.com/2023/11/13/202311131630/
License:
Copyright (c) 2023 CC-BY-NC-4.0 LICENSE