一些Linux网络编程的操作和姿势
Linux网络编程 字节序 字节序:字节在内存中存储的顺序
小端字节序:数据的高位字节存储在内存的高位地址,低位字节存储在内存的地位地址
大端字节序:数据的低位字节存储在内存的高位地址,高位字节存储在内存的地位地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> int main () { union { short value; char bytes[sizeof (short )]; }test; test.value = 0x0102 ; if (test.bytes[0 ] == 1 && (test.bytes[1 ] == 2 )){ printf ("大端序\n" ); }else if (test.bytes[0 ] == 2 && (test.bytes[1 ] == 1 )){ printf ("小端序\n" ); }else { printf ("未知\n" ); } return 0 ; }
网络字节顺序 是TCPIP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释,网络字节顺序采用大端排序方式 。
#include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; uint16_t ntohs (uint16_t netshort) ; uint32_t htol (uint32_t hostlong) ; uint32_t ntohl (uint32_t netlong) ;
示例程序
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 #include <arpa/inet.h> #include <stdio.h> int main () { unsigned short a = 0x0102 ; printf ("%04x\n" , a); unsigned short b = htons(a); printf ("%04x\n" , b); printf ("=================\n" ); char buf[4 ] = {192 , 168 , 1 , 100 }; int num = *(int *)buf; int sum = htonl(num); unsigned char *p = (char *)∑ printf ("%d %d %d %d\n" , *p, *(p+1 ), *(p+2 ), *(p+3 )); printf ("=================\n" ); unsigned char buf1[4 ] = {1 , 1 , 168 , 192 }; int num1 = *(int *)buf1; int sum1 = ntohl(num1); unsigned char *p1 = (unsigned char *)&sum1; printf ("%d %d %d %d\n" , *p1, *(p1+1 ), *(p1+2 ), *(p1+3 )); return 0 ; }
socket地址 socket地址是一个结构体,封装端口号和IP等信息,后面的socket相关的API中需要使用到这个socket地址。
#include <bits/socket.h> struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; };typedef unsigned short int sa_family_t ; struct sockaddr_storage { sa_family_t sa_family; unsigned long int __ss_align; char __ss_padding[ 128 - sizeof (__ss_align) ]; };typedef unsigned short int sa_family_t ;
专用socket地址 通常使用中采用如下专用socket地址格式,其中struct sockaddr_in
最为常用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr; unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE - sizeof (in_port_t ) - sizeof (struct in_addr)]; };struct in_addr { in_addr_t s_addr; };struct sockaddr_in6 { sa_family_t sin6_family; in_port_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr; uint32_t sin6_scope_id; };typedef unsigned short uint16_t ;typedef unsigned int uint32_t ;typedef uint16_t in_port_t ;typedef uint32_t in_addr_t ;#define __SOCKADDR_COMMON_SIZE (sizeof(unsigned short int))
所有专用 socket 地址(以及 sockaddr_storage)类型的变量在实际使用时都需要转化为通用 socket 地址类型 sockaddr(强制转化即可),因为所有 socket 编程接口使用的地址参数类型都是 sockaddr。
IP地址转换(字符串ip-整数,主机、网络字节序转换) #include <arpa/inet.h> int inet_pton (int af, const char *src, void *dst) ; af: 地址族: AF_INET AF_INET6 src: 需要转换的点分十进制的IP字符串 dst: 传出参数,转换后的结果保存在这个里面const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ; af:地址族: AF_INET AF_INET6 src: 要转换的ip的整数的地址 dst: 转换成IP地址字符串保存的地方 size:第三个参数的大小(数组的大小) 返回值:返回转换后的数据的地址(字符串),和 dst 是一样的
套接字函数 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 #include <sys/types.h> #include <sys/socket.h> #include <arpa/inet.h> int socket (int domain, int type, int protocol) ; - 功能:创建一个套接字 - 参数: - domain: 协议族 AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) - type: 通信过程中使用的协议类型 SOCK_STREAM : 流式协议 SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ; 名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 int listen (int sockfd, int backlog) ; - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket ()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 5 int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ; - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port) - addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败 int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ; - 功能: 客户端连接服务器 - 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息 - addrlen : 第二个参数的内存大小 - 返回值:成功 0 , 失败 -1 ssize_t write (int fd, const void *buf, size_t count) ; ssize_t read (int fd, void *buf, size_t count) ;
TCP通信并发 要实现TCP通信服务器并发的任务,使用多线程/多进程来解决。
思路:
一个父进程,多个子进程
父进程负责等待并接收客户端连接
子进程:完成通信,接受一个客户端连接,就创建一个子进程用于通信
详细代码参考我的github:linux_learn/socket at main · b4158813/linux_learn (github.com)
半关闭 & 端口复用 半关闭
当 TCP 链接中 A 向 B 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FIN 给 A,A 方处于半连接状态(半开关),此时 A 可以接收 B 发 送的数据,但是 A 已经不能再向 B 发送数据。
从程序的角度,可以使用 API 来控制实现半连接状态:
#include <sys/socket.h> int shutdown (int sockfd, int how) ; sockfd: 需要关闭的socket的描述符 how: 允许为shutdown操作选择以下几种方式: SHUT_RD(0 ): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。 该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 SHUT_WR(1 ): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。 SHUT_RDWR(2 ):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。
使用 close 中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用 计数为 0 时才关闭连接。shutdown 不考虑描述符的引用计数,直接关闭描述符。也可选择中止一个方 向的连接,只中止读或只中止写。
注意:
如果有多个进程共享一个套接字,close 每被调用一次,计数减 1 ,直到计数为 0 时,也就是所用 进程都调用了 close,套接字将被释放。
在多进程中如果一个进程调用了 shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。 但如果一个进程 close(sfd) 将不会影响到其它进程。
端口复用
端口复用最常用的用途是:
防止服务器重启时之前绑定的端口还未释放
程序突然退出而系统没有释放端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <sys/types.h> #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void *optval, socklen_t optlen) ; 参数: - sockfd : 要操作的文件描述符 - level : 级别,SOL_SOCKET(端口复用的级别) - optname : 选项的名称 - SO_REUSEADDR - SO_REUSEPORT - optval : 端口复用的值(整形) - 1 : 可以复用 - 0 : 不可以复用 - optlen : optval参数的大小 端口复用,设置的时机是在服务器绑定端口之前。 setsockopt() bind()
查看网络相关信息的命令:
netstat 参数: -a 所有的socket -p 显示正在使用socket的程序的名称 -n 直接使用IP地址,而不通过域名服务器
I/O多路复用(多路转接) I/O多路复用能使得程序能同时监听多个文件描述符,能够提高程序性能,Linux下实现I/O多路复用的系统调用主要有 select, poll, epoll
select
主旨思想:
首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或者多个进行I/O 操作时,该函数才返回。
a. 这个函数是阻塞
b. 函数对文件描述符的检测的操作是由内核完成的
在返回时,它会告诉进程有多少(哪些)描述符要进行I/O操作。
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 #include <sys/time.h> #include <sys/types.h> #include <unistd.h> #include <sys/select.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ; - 参数: - nfds : 委托内核检测的最大文件描述符的值 + 1 - readfds : 要检测的文件描述符的读的集合,委托内核检测哪些文件描述符的读的属性 - 一般检测读操作 - 对应的是对方发送过来的数据,因为读是被动的接收数据,检测的就是读缓冲区 - 是一个传入传出参数 - writefds : 要检测的文件描述符的写的集合,委托内核检测哪些文件描述符的写的属性 - 委托内核检测写缓冲区是不是还可以写数据(不满的就可以写) - exceptfds : 检测发生异常的文件描述符的集合 - timeout : 设置的超时时间 struct timeval { long tv_sec; long tv_usec; }; - NULL : 永久阻塞,直到检测到了文件描述符有变化 - tv_sec = 0 tv_usec = 0 , 不阻塞 - tv_sec > 0 tv_usec > 0 , 阻塞对应的时间 - 返回值 : - -1 : 失败 - >0 (n) : 检测的集合中有n个文件描述符发生了变化 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 ) ;
select缺点:
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大 (O(n)遍历)
select支持的文件描述符数量太小了,默认是1024
fds集合不能重用,每次都需要重置
select 服务端
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <unistd.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); fd_set rdset, tmp; FD_ZERO(&rdset); FD_SET(lfd, &rdset); int maxfd = lfd; printf ("maxfd = %d\n" , maxfd); while (1 ){ tmp = rdset; int ret = select(maxfd + 1 , &tmp, NULL , NULL , NULL ); if (ret == -1 ){ perror("select" ); exit (-1 ); }else if (ret == 0 ){ continue ; }else if (ret > 0 ){ if (FD_ISSET(lfd, &tmp)){ struct sockaddr_in caddr; int len = sizeof (caddr); int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); FD_SET(cfd, &rdset); maxfd = maxfd > cfd ? maxfd : cfd; } for (int i = lfd + 1 ; i <= maxfd; ++ i){ if (FD_ISSET(i, &tmp)){ char buf[1024 ] = {0 }; int len = read(i, buf, sizeof buf); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); close(i); FD_CLR(i, &rdset); }else if (len > 0 ){ printf ("read buf = %s\n" , buf); write(i, buf, strlen (buf) + 1 ); } } } } } close(lfd); return 0 ; }
poll 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <poll.h> struct pollfd { int fd; short events; short revents; };struct pollfd myfd ; myfd.fd = 5 ; myfd.events = POLLIN | POLLOUT;int poll (struct pollfd *fds, nfds_t nfds, int timeout) ; - 参数: - fds : 是一个struct pollfd 结构体数组,这是一个需要检测的文件描述符的集合 - nfds : 这个是第一个参数数组中最后一个有效元素的下标 + 1 - timeout : 阻塞时长 - 0 : 不阻塞 - 1 : 阻塞,当检测到需要检测的文件描述符有变化,解除阻塞 - > 0 : 阻塞的时长 - 返回值: -1 : 失败 >0 (n) : 成功,n表示检测到集合中有n个文件描述符发生变化
poll 服务端
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <unistd.h> #include <poll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); struct pollfd fds [1024]; for (int i = 0 ; i < 1024 ; ++ i){ fds[i].fd = -1 ; fds[i].events = POLLIN; } fds[0 ].fd = lfd; int nfds = 0 ; while (1 ){ int ret = poll(fds, nfds + 1 , -1 ); if (ret == -1 ){ perror("select" ); exit (-1 ); }else if (ret == 0 ){ continue ; }else if (ret > 0 ){ if (fds[0 ].revents & POLLIN){ struct sockaddr_in caddr; int len = sizeof (caddr); int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); char cip[16 ]; inet_ntop(AF_INET, &caddr.sin_addr.s_addr, cip, sizeof cip); unsigned short cport = ntohs(caddr.sin_port); printf ("new request accepted, IP = %s, port = %d\n" , cip, cport); for (int i = 1 ; i < 1024 ; ++ i){ if (fds[i].fd == -1 ){ fds[i].fd = cfd; fds[i].events = POLLIN; break ; } } nfds = nfds > cfd ? nfds : cfd; } for (int i = 1 ; i <= nfds; ++ i){ if (fds[i].revents & POLLIN){ char buf[1024 ] = {0 }; int len = read(fds[i].fd, buf, sizeof buf); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); close(fds[i].fd); fds[i].fd = -1 ; }else if (len > 0 ){ printf ("read buf = %s\n" , buf); write(fds[i].fd, buf, strlen (buf) + 1 ); } } } } } close(lfd); return 0 ; }
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 #include <sys/epoll.h> int epoll_create (int size) ; - 参数: size : 目前没有意义了。随便写一个数,必须大于0 - 返回值: -1 : 失败 > 0 : 文件描述符,操作epoll实例的 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ;struct epoll_event { uint32_t events; epoll_data_t data; }; 常见的Epoll检测事件: - EPOLLIN - EPOLLOUT - EPOLLERR ... int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ; - 参数: - epfd : epoll实例对应的文件描述符 - op : 要进行什么操作 EPOLL_CTL_ADD: 添加 EPOLL_CTL_MOD: 修改 EPOLL_CTL_DEL: 删除 - fd : 要检测的文件描述符 - event : 检测文件描述符什么事情 int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout) ; - 参数: - epfd : epoll实例对应的文件描述符 - events : 传出参数,保存了发送了变化的文件描述符的信息 - maxevents : 第二个参数结构体数组的大小 - timeout : 阻塞时间 - 0 : 不阻塞 - -1 : 阻塞,直到检测到fd数据发生变化,解除阻塞 - > 0 : 阻塞的时长(毫秒) - 返回值: - 成功,返回发生变化的文件描述符的个数 > 0 - 失败 -1
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <unistd.h> #include <sys/epoll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); struct epoll_event epevs [1024]; while (1 ){ int ret = epoll_wait(epfd, epevs, 1024 , -1 ); if (ret == -1 ){ perror("epoll_wait" ); exit (-1 ); } printf ("res = %d\n" , ret); for (int i = 0 ; i < ret; ++ i){ int curfd = epevs[i].data.fd; if (curfd == lfd){ struct sockaddr_in caddr ; int len = sizeof (caddr); int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); epev.events = EPOLLIN; epev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); }else { if (epevs[i].events & EPOLLOUT){ continue ; } char buf[1024 ] = {0 }; int len = read(curfd, buf, sizeof buf); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL ); close(curfd); }else if (len > 0 ){ printf ("read buf = %s\n" , buf); write(curfd, buf, strlen (buf) + 1 ); } } } } close(lfd); close(epfd); return 0 ; }
epoll的工作模式
LT模式(水平触发)
假设委托内核检测读事件 => 检测到fd的读缓冲区
读缓冲区有数据 => epoll检测到了会给用户通知
a. 用户不读数据,数据一直在缓冲区,epoll会一直通知
b. 用户只读了一部分数据,epoll也会继续通知
c. 缓冲区的数据读完了,epoll不通知
LT(level - triggered)是缺省的工作方式,并且同时支持 block 和 no-block socket。在这 种做中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式(边沿触发)
假设委托内核检测读事件 => 检测到fd的读缓冲区
读缓冲区有数据 => epoll检测到了会给用户通知
a. 用户不读数据,数据一直在缓冲区中,epoll下次检测时不通知
b. 用户读了一部分数据,epoll下一次也不会通知
c. 缓冲区的数据读完了,epoll不通知
ET(edge - triggered)是高速工作方式,只支持 no-block socket。在这种模式下,当描述 符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪, 并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述 符不再为就绪状态了。但是请注意,如果一直不对这个 fd 作 IO 操作(从而导致它再次变成 未就绪),内核不会发送更多的通知(only once)。
ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写 操作把处理多个文件描述符的任务饿死。
struct epoll_event { uint32_t events; epoll_data_t data; }; 常见的Epoll检测事件: - EPOLLIN - EPOLLOUT - EPOLLERR - EPOLLET
LT模式 服务端
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <unistd.h> #include <sys/epoll.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); struct epoll_event epevs [1024]; while (1 ){ int ret = epoll_wait(epfd, epevs, 1024 , -1 ); if (ret == -1 ){ perror("epoll_wait" ); exit (-1 ); } printf ("res = %d\n" , ret); for (int i = 0 ; i < ret; ++ i){ int curfd = epevs[i].data.fd; if (curfd == lfd){ struct sockaddr_in caddr ; int len = sizeof (caddr); int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); epev.events = EPOLLIN; epev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); }else { if (epevs[i].events & EPOLLOUT){ continue ; } char buf[5 ] = {0 }; int len = read(curfd, buf, sizeof buf); if (len == -1 ){ perror("read" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL ); close(curfd); }else if (len > 0 ){ printf ("read buf = %s\n" , buf); write(curfd, buf, strlen (buf) + 1 ); } } } } close(lfd); close(epfd); return 0 ; }
ET模式 服务端
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 #include <stdio.h> #include <arpa/inet.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <unistd.h> #include <sys/epoll.h> #include <fcntl.h> #include <errno.h> int main () { int lfd = socket(PF_INET, SOCK_STREAM, 0 ); struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_addr.s_addr = INADDR_ANY; saddr.sin_port = htons(9999 ); bind(lfd, (struct sockaddr *)&saddr, sizeof (saddr)); listen(lfd, 8 ); int epfd = epoll_create(100 ); struct epoll_event epev ; epev.events = EPOLLIN; epev.data.fd = lfd; epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev); struct epoll_event epevs [1024]; while (1 ){ int ret = epoll_wait(epfd, epevs, 1024 , -1 ); if (ret == -1 ){ perror("epoll_wait" ); exit (-1 ); } printf ("res = %d\n" , ret); for (int i = 0 ; i < ret; ++ i){ int curfd = epevs[i].data.fd; if (curfd == lfd){ struct sockaddr_in caddr ; int len = sizeof (caddr); int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); int flag = fcntl(cfd, F_GETFL); flag |= O_NONBLOCK; fcntl(cfd, F_SETFL, flag); epev.events = EPOLLIN | EPOLLET; epev.data.fd = cfd; epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev); }else { if (epevs[i].events & EPOLLOUT){ continue ; } char buf[5 ]; int len = 0 ; while ((len = read(curfd, buf, sizeof (buf))) > 0 ){ printf ("recv data : %s\n" , buf); write(curfd, buf, len); } if (len == 0 ){ printf ("client closed...\n" ); }else if (len == -1 ){ if (errno == EAGAIN){ printf ("data over...\n" ); }else { perror("read" ); exit (-1 ); } } } } } close(lfd); close(epfd); return 0 ; }
UDP通信实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #include <sys/types.h> #include <sys/socket.h> ssize_t sendto (int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen) ; - 参数: - sockfd : 通信的fd - buf : 要发送的数据 - len : 发送数据的长度 - flags : 0 - dest_addr : 通信的另外一端的地址信息 - addrlen : 地址的内存大小 ssize_t recvfrom (int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen) ; - 参数: - sockfd : 通信的fd - buf : 接收数据的数组 - len : 数组的大小 - flags : 0 - src_addr : 用来保存另外一端的地址信息,不需要可以指定为NULL - addrlen : 地址的内存大小
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 51 52 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999 ); saddr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr *)&saddr, sizeof saddr); if (ret == -1 ){ perror("bind" ); exit (-1 ); } while (1 ){ char recvbuf[128 ]; struct sockaddr_in caddr ; int len = sizeof caddr; int num = recvfrom(fd, recvbuf, sizeof recvbuf, 0 , (struct sockaddr *)&caddr, &len); char ipbuf[16 ]; printf ("client IP: %s, Port: %d\n" , inet_ntop(AF_INET, &caddr.sin_addr.s_addr, ipbuf, sizeof ipbuf), ntohs(caddr.sin_port) ); printf ("client say: %s\n" , recvbuf); sendto(fd, recvbuf, strlen (recvbuf) + 1 , 0 , (struct sockaddr *)&caddr, sizeof caddr); } close(fd); return 0 ; }
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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in saddr ; saddr.sin_family = AF_INET; saddr.sin_port = htons(9999 ); inet_pton(AF_INET, "172.18.0.1" , &saddr.sin_addr.s_addr); int num = 0 ; while (1 ){ char sendbuf[128 ]; sprintf (sendbuf, "hello, I am client %d\n" , num++); sendto(fd, sendbuf, strlen (sendbuf) + 1 , 0 , (struct sockaddr *)&saddr, sizeof saddr); int num = recvfrom(fd, sendbuf, sizeof sendbuf, 0 , NULL , NULL ); printf ("server say: %s\n" , sendbuf); sleep(1 ); } close(fd); return 0 ; }
广播
向子网中多台计算机发送消息,并且子网中所有的计算机都可以接收到发送方发送的消息,每个广 播消息都包含一个特殊的IP地址,这个IP中子网内主机标志部分的二进制全部为1。
a.只能在局域网中使用。
b.客户端需要绑定服务器广播使用的端口,才可以接收到广播消息。
int setsockopt (int sockfd, int level, int optname,const void *optval, socklen_t optlen) ; - sockfd : 文件描述符 - level : SOL_SOCKET - optname : SO_BROADCAST - optval : int 类型的值,为1 表示允许广播 - optlen : optval的大小
广播 服务端
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(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } int op = 1 ; setsockopt(fd, SOL_SOCKET, SO_BROADCAST, &op, sizeof op); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9999 ); inet_pton(AF_INET, "172.18.255.255" , &cliaddr.sin_addr.s_addr); int num = 0 ; while (1 ){ char sendbuf[128 ]; sprintf (sendbuf, "hello, client... %d\n" , num++); sendto(fd, sendbuf, strlen (sendbuf) + 1 , 0 , (struct sockaddr *)&cliaddr, sizeof cliaddr); printf ("广播的数据:%s\n" , sendbuf); sleep(1 ); } close(fd); 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(9999 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr *)&addr, sizeof addr); if (ret == -1 ){ perror("bind" ); exit (-1 ); } while (1 ){ char buf[128 ]; int num = recvfrom(fd, buf, sizeof buf, 0 , NULL , NULL ); printf ("server say: %s\n" , buf); } close(fd); return 0 ; }
组播(多播)
单播地址标识单个 IP 接口,广播地址标识某个子网的所有 IP 接口,多播地址标识一组 IP 接口。 单播和广播是寻址方案的两个极端(要么单个要么全部),多播则意在两者之间提供一种折中方 案。多播数据报只应该由对它感兴趣的接口接收,也就是说由运行相应多播会话应用系统的主机上 的接口接收。另外,广播一般局限于局域网内使用,而多播则既可以用于局域网,也可以跨广域网使用。
a.组播既可以用于局域网,也可以用于广域网
b.客户端需要加入多播组,才能接收到多播的数据
组播地址
IP 多播通信必须依赖于 IP 多播地址,在 IPv4 中它的范围从 224.0.0.0 到 239.255.255.255 , 并被划分为局部链接多播地址、预留多播地址和管理权限多播地址三类
设置组播:
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 int setsockopt (int sockfd, int level, int optname,const void *optval, socklen_t optlen) ; 2. 本地套接字 本地套接字的作用:本地的进程间通信 有关系的进程间的通信 没有关系的进程间的通信 本地套接字实现流程和网络套接字类似,一般呢采用TCP的通信流程。 - level : IPPROTO_IP - optname : IP_MULTICAST_IF - optval : struct in_addr // 客户端加入到多播组: - level : IPPROTO_IP - optname : IP_ADD_MEMBERSHIP - optval : struct ip_mreq struct ip_mreq { struct in_addr imr_multiaddr ; struct in_addr imr_interface ; };typedef uint32_t in_addr_t ;struct in_addr { in_addr_t s_addr; };
多播 服务端
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 <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct in_addr imr_multiaddr ; inet_pton(fd, "239.0.0.10" , &imr_multiaddr.s_addr); setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &imr_multiaddr, sizeof imr_multiaddr); struct sockaddr_in cliaddr ; cliaddr.sin_family = AF_INET; cliaddr.sin_port = htons(9999 ); inet_pton(AF_INET, "239.0.0.10" , &cliaddr.sin_addr.s_addr); int num = 0 ; while (1 ){ char sendbuf[128 ]; sprintf (sendbuf, "hello, client... %d\n" , num++); sendto(fd, sendbuf, strlen (sendbuf) + 1 , 0 , (struct sockaddr *)&cliaddr, sizeof cliaddr); printf ("多播的数据:%s\n" , sendbuf); sleep(1 ); } close(fd); 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 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(PF_INET, SOCK_DGRAM, 0 ); if (fd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(9999 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr *)&addr, sizeof addr); if (ret == -1 ){ perror("bind" ); exit (-1 ); } struct ip_mreq op ; inet_pton(AF_INET, "239.0.0.10" , &op.imr_multiaddr.s_addr); op.imr_interface.s_addr = INADDR_ANY; setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &op, sizeof op); while (1 ){ char buf[128 ]; int num = recvfrom(fd, buf, sizeof buf, 0 , NULL , NULL ); printf ("server say: %s\n" , buf); } close(fd); return 0 ; }
本地套接字
本地套接字的作用:本地的进程间通信
有关系的进程间的通信
没有关系的进程间的通信
本地套接字实现流程和网络套接字类似,一般采用TCP的通信流程。
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 1. 创建监听的套接字 int lfd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0 );2. 监听的套接字绑定本地的套接字文件 -> server端 struct sockaddr_un addr ; bind(lfd, addr, len);3. 监听 listen(lfd, 100 );4. 等待并接受连接请求 struct sockaddr_un cliaddr ; int cfd = accept(lfd, &cliaddr, len);5. 通信 接收数据:read/recv 发送数据:write/send 6. 关闭连接 close();1. 创建通信的套接字 int fd = socket(AF_UNIX/AF_LOCAL, SOCK_STREAM, 0 );2. 监听的套接字绑定本地的IP 端口 struct sockaddr_un addr ; bind(lfd, addr, len);3. 连接服务器 struct sockaddr_un serveraddr ; connect(fd, &serveraddr, sizeof (serveraddr));4. 通信 接收数据:read/recv 发送数据:write/send 5. 关闭连接 close();
本地套接字通信 服务端
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 <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/un.h> int main () { unlink("server.sock" ); int lfd = socket(PF_LOCAL, SOCK_STREAM, 0 ); if (lfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "server.sock" ); int ret = bind(lfd, (struct sockaddr *)&addr, sizeof addr); if (ret == -1 ){ perror("bind" ); exit (-1 ); } ret = listen(lfd, 100 ); if (ret == -1 ){ perror("listen" ); exit (-1 ); } struct sockaddr_un caddr ; int len = sizeof caddr; int cfd = accept(lfd, (struct sockaddr *)&caddr, &len); if (cfd == -1 ){ perror("accept" ); exit (-1 ); } printf ("client socket filename: %s\n" , caddr.sun_path); while (1 ){ char buf[128 ]; int len = recv(cfd, buf, sizeof buf, 0 ); if (len == -1 ){ perror("recv" ); exit (-1 ); }else if (len == 0 ){ printf ("client closed...\n" ); break ; }else if (len > 0 ){ printf ("client say: %s\n" , buf); send(cfd, buf, len, 0 ); } } close(cfd); 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 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/un.h> int main () { unlink("client.sock" ); int cfd = socket(PF_LOCAL, SOCK_STREAM, 0 ); if (cfd == -1 ){ perror("socket" ); exit (-1 ); } struct sockaddr_un addr ; addr.sun_family = AF_LOCAL; strcpy (addr.sun_path, "client.sock" ); int ret = bind(cfd, (struct sockaddr *)&addr, sizeof addr); if (ret == -1 ){ perror("bind" ); exit (-1 ); } struct sockaddr_un saddr ; saddr.sun_family = AF_LOCAL; strcpy (saddr.sun_path, "server.sock" ); ret = connect(cfd, (struct sockaddr *)&saddr, sizeof saddr); if (ret == -1 ){ perror("connect" ); exit (-1 ); } printf ("server socket filename: %s\n" , saddr.sun_path); int num = 0 ; while (1 ){ char buf[128 ]; sprintf (buf, "hello, I am client %d\n" , num++); send(cfd, buf, strlen (buf) + 1 , 0 ); printf ("client say: %s\n" , buf); int len = recv(cfd, buf, sizeof buf, 0 ); if (len == -1 ){ perror("send" ); exit (-1 ); }else if (len == 0 ){ printf ("server closed...\n" ); break ; }else if (len > 0 ){ printf ("server say: %s\n" , buf); } sleep(1 ); } close(cfd); return 0 ; }