Unix 网络编程 视频笔记部分 协议 了解啥是协议,大概就是 规则 的意思,数据传输和解释时的规则,毕竟数据传输的时候本质是二进制,要按一定的格式才能解释过来
典型协议 :
传输层 常见协议有TCP/UDP协议。
应用层 常见的协议有HTTP协议,FTP协议。
网络层 常见协议有IP协议、ICMP协议、IGMP协议。
网络接口层 常见协议有ARP协议、RARP协议。
TCP传输控制协议 (Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层 通信协议。
UDP用户数据报协议(User Datagram Protocol)是OSI 参考模型中一种无连接的传输层 协议,提供面向事务的简单不可靠信息传送服务。
HTTP超文本传输协议 (Hyper Text Transfer Protocol)是互联网 上应用最为广泛的一种网络协议 。
FTP文件传输协议(File Transfer Protocol)
IP协议是因特网 互联协议(Internet Protocol)
ICMP协议是Internet控制报文 协议(Internet Control Message Protocol)它是TCP/IP协议族 的一个子协议,用于在IP主机 、路由 器之间传递控制消息。
IGMP协议是 Internet 组管理协议(Internet Group Management Protocol),是因特网协议家族中的一个组播协议。该协议运行在主机和组播路由器之间。
ARP 协议是正向地址解析协议 (Address Resolution Protocol),通过已知的IP,寻找对应主机的MAC地址 。
RARP 是反向地址转换协议,通过MAC地址确定IP地址。
网络应用程序设计模式 C/S模式
传统的网络应用设计模式,客户机(client)/服务器(server)模式。需要在通讯两端各自部署客户机和服务器来完成数据通信。
B/S模式
浏览器()/服务器(server)模式。只需在一端部署服务器,而另外一端使用每台PC都默认配置的浏览器即可完成数据的传输。
优缺点,cs模式,开发量大,采用的协议相对灵活,数据传输效率高,因为提前缓存了数据在客户端;bs模式,开发量相对于cs模式小,但采用的协议固定
分层模型 OSI七层模型
物数网传会表应
物理层 :主要定义物理设备标准,如网线的接口类型、光纤的接口类型、各种传输介质的传输速率等。它的主要作用是传输比特流(就是由1、0转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是我们常说的数模转换与模数转换)。这一层的数据叫做比特。
数据链路层 :定义了如何让格式化数据以帧为单位进行传输,以及如何让控制对物理介质的访问。这一层通常还提供错误检测和纠正,以确保数据的可靠传输。如:串口通信中使用到的115200、8、N、1
网络层 :在位于不同地理位置的网络中的两个主机系统之间提供连接和路径选择。Internet的发展使得从世界各站点访问信息的用户数大大增加,而网络层正是管理这种连接的层。
传输层 :定义了一些传输数据的协议和端口号(WWW端口80等),如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP特性恰恰相反,用于传输可靠性要求不高,数据量小的数据,如QQ聊天数据就是通过这种方式传输的)。 主要是将从下层接收的数据进行分段和传输,到达目的地址后再进行重组。常常把这一层数据叫做段。
会话层 :通过传输层(端口号:传输端口与接收端口)建立数据传输的通路。主要在你的系统之间发起会话或者接受会话请求(设备之间需要互相认识可以是IP也可以是MAC或者是主机名)。
表示层 :可确保一个系统的应用层所发送的信息可以被另一个系统的应用层读取。例如,PC程序与另一台计算机进行通信,其中一台计算机使用扩展二一十进制交换码(EBCDIC),而另一台则使用美国信息交换标准码(ASCII)来表示相同的字符。如有必要,表示层会通过使用一种通格式来实现多种数据格式之间的转换。
应用层 :是最靠近用户的OSI层。这一层为用户的应用程序(例如电子邮件、文件传输和终端仿真)提供网络服务。
TCP/IP四层模型
通信过程 简单来说就是数据一层层的封包,然后进入网络,到达目的地后,再一层层的解包
协议格式 数据包封装
数据-应用-传输-网络-链路 类似如此封装
传输层及其以下的机制由内核提供,应用层由用户进程提供(后面将介绍如何使用socket API编写应用程序),应用程序对通讯数据的含义进行解释,而传输层及其以下处理通讯的细节,将数据从一台计算机通过一定的路径发送到另一台计算机。应用层数据通过协议栈发到网络上时,每层协议都要加上一个数据首部(header),称为封装
以太网帧格式
格式如下,类型有三种:
基础格式: [ [目的地址(6)] [源地址6] [类型2] [数据46-1500] [crc4]]
类型0800: [ [目的地址(6)] [源地址6] [0800] [数据46-1500] [crc4]]
类型0806: [ [目的地址(6)] [源地址6] [0806] [ARP请求/应答(28) + PAD(18)] [crc4]]
类型8035: [ [目的地址(6)] [源地址6] [类型2] [RARP请求/应答(28) + PAD(18)] [crc4]]
不同的类型用于路由器寻路的时候,获取下一跳的mac地址
ARP数据报格式
在网络通讯时,源主机的应用程序知道目的主机的IP地址和端口号,却不知道目的主机的硬件地址,而数据包首先是被网卡接收到再去处理上层协议的,如果接收到的数据包的硬件地址与本机不符,则直接丢弃。因此在通讯前必须获得目的主机的硬件地址。ARP协议就起到这个作用。源主机发出ARP请求,询问“IP地址是192.168.0.1的主机的硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部的硬件地址填FF:FF:FF:FF:FF:FF表示广播),目的主机接收到广播的ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填写在应答包中。
IP段格式
UDP数据报格式
NAT映射 打洞机制 这里简单了解一下,NAT映射,内网IP由路由器到公网IP之间的映射,因为内网IP外部访问不到,打洞机制,就在目的地和起源地之间直接创建一条通路,借由某公网IP
套接字 成对出现,和FIFO有点像,但是是全双工的,且一个socket文件对应两个缓冲区,一个读缓冲区,一个写缓冲区
然后就是一些函数的使用 如:
socket 创建套接字
bind 绑定IP和端口
listen 指定最大同时发起连接数
accept 阻塞等待客户端发起连接
connect 发起连接
然后常见的C/S模型:
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 #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <ctype.h> #define SERV_IP "172.23.155.242" #define SERV_PORT 6666 int main () { int lfd, cfd; struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; int n, ret; char buf[BUFSIZ], clie_IP[BUFSIZ]; lfd = socket (AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror ("socket error" ); exit (1 ); } serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons (SERV_PORT); inet_pton (AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); ret = bind (lfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); if (ret == -1 ) { perror ("bind error" ); exit (1 ); } ret = listen (lfd, 32 ); if (ret == -1 ) { perror ("listen error" ); exit (-1 ) ; } clie_addr_len = sizeof (clie_addr); cfd = accept (lfd, (struct sockaddr*)&clie_addr, &clie_addr_len); if (cfd == -1 ) { perror ("accept error" ); exit (1 ); } printf ("connect success\n" ); printf ("client IP:%s, client port:%d\n" , inet_ntop (AF_INET, &clie_addr.sin_addr.s_addr, clie_IP, sizeof (clie_IP)), ntohs (clie_addr.sin_port)); while (1 ) { n = read (cfd, buf, sizeof (buf)); for (int i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); write (cfd, buf, n); } close (lfd); close (cfd); return 0 ; } #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include <arpa/inet.h> #define SERV_IP "172.23.155.242" #define SERV_PORT 6666 int main () { int cfd; struct sockaddr_in serv_addr; char buf[BUFSIZ]; int n; cfd = socket (AF_INET, SOCK_STREAM, 0 ); if (cfd == -1 ) { perror ("socket error" ); exit (1 ); } memset (&serv_addr, 0 , sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_port = htons (SERV_PORT); inet_pton (AF_INET, SERV_IP, &serv_addr.sin_addr.s_addr); connect (cfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); while (1 ) { fgets (buf, sizeof (buf), stdin); write (cfd, buf, strlen (buf)); n = read (cfd, buf, sizeof (buf)); write (STDOUT_FILENO, buf, n); } close (cfd); }
三次握手/四次握手 三次握手发生在建立连接时期,1.主动发起方发送SYN请求 2.被动接收方ACK应答,并发送SYN请求 3.主动发起方ACK应答 三次握手完成
四次握手发送在关闭时期, 1.主动关闭方发送FIN请求 2.被动接收方ACK应答 3.被动接收方发送FIN请求并再次发送ACK应答 4.主动关闭方ACK应答
建立连接的过程是三方握手,而关闭连接通常需要4个段,服务器的应答和关闭连接请求通常不合并在一个段中 ,因为有连接半关闭 的情况,这种情况下客户端关闭连接之后就不能再发送数据给服务器了,但是服务器还可以发送数据给客户端,直到服务器也关闭连接为止。
MTU 通信术语:最大传输单元
是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。
以下是一些协议的MTU:
FDDI协议:4352字节
以太网(Ethernet)协议:1500字节
PPPoE(ADSL)协议:1492字节
X.25协议(Dial Up/Modem):576字节
Point-to-Point:4470字节
ip地址: 65535
mss 受MTU影响,除开协议头以外,纯数据部分所占多大
滑动窗口(TCP流量控制)
发送端发起连接,声明最大段尺寸是1460,初始序号是0,窗口大小是4K,表示“我的接收缓冲区还有4K字节空闲,你发的数据不要超过4K”。接收端应答连接请求,声明最大段尺寸是1024,初始序号是8000,窗口大小是6K。发送端应答,三方握手结束。
发送端发出段4-9,每个段带1K的数据,发送端根据窗口大小知道接收端的缓冲区满了,因此停止发送数据。
接收端的应用程序提走2K数据,接收缓冲区又有了2K空闲,接收端发出段10,在应答已收到6K数据的同时声明窗口大小为2K。
接收端的应用程序又提走2K数据,接收缓冲区有4K空闲,接收端发出段11,重新声明窗口大小为4K。
发送端发出段12-13,每个段带2K数据,段13同时还包含FIN位。
接收端应答接收到的2K数据(6145-8192),再加上FIN位占一个序号8193,因此应答序号是8194,连接处于半关闭状态,接收端同时声明窗口大小为2K。
接收端的应用程序提走2K数据,接收端重新声明窗口大小为4K。
接收端的应用程序提走剩下的2K数据,接收缓冲区全空,接收端重新声明窗口大小为6K。
接收端的应用程序在提走全部数据后,决定关闭连接,发出段17包含FIN位,发送端应答,连接完全关闭。
TCP状态转换 用netstat -apn 可以查看端口的部分状态,如之前经常见到的 TIME_WAIT
实线表示主动发起方,虚线表示被动接受方,细线表示某些接受和发送同时进行。基本就是三次握手和四次握手直接,状态变化的具体过程
CLOSED :表示初始状态。
LISTEN :该状态表示服务器端的某个SOCKET处于监听状态,可以接受连接。
SYN_SENT :这个状态与SYN_RCVD遥相呼应,当客户端SOCKET执行CONNECT连接时,它首先发送SYN报文,随即进入到了SYN_SENT状态,并等待服务端的发送三次握手中的第2个报文。SYN_SENT状态表示客户端已发送SYN报文。
SYN_RCVD: 该状态表示接收到SYN报文,在正常情况下,这个状态是服务器端的SOCKET在建立TCP连接时的三次握手会话过程中的一个中间状态,很短暂。此种状态时,当收到客户端的ACK报文后,会进入到ESTABLISHED状态。
ESTABLISHED: 表示连接已经建立。
FIN_WAIT_1: FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。区别是:
FIN_WAIT_1状态是当socket在ESTABLISHED状态时,想主动关闭连接,向对方发送了FIN报文,此时该socket进入到FIN_WAIT_1状态。
FIN_WAIT_2状态是当对方回应ACK后,该socket进入到FIN_WAIT_2状态,正常情况下,对方应马上回应ACK报文,所以FIN_WAIT_1状态一般较难见到,而FIN_WAIT_2状态可用netstat看到。
FIN_WAIT_2:主动关闭链接的一方,发出FIN收到ACK以后进入该状态。称之为半连接或半关闭状态。 该状态下的socket只能接收数据,不能发。
TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,等2MSL后即可回到CLOSED可用状态。如果FIN_WAIT_1状态下,收到对方同时带 FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。
CLOSING: 这种状态较特殊,属于一种较罕见的状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
CLOSE_WAIT: 此种状态表示在等待关闭。当对方关闭一个SOCKET后发送FIN报文给自己,系统会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,察看是否还有数据发送给对方,如果没有可以 close这个SOCKET,发送FIN报文给对方,即关闭连接。所以在CLOSE_WAIT状态下,需要关闭连接。
LAST_ACK: 该状态是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可以进入到CLOSED可用状态。
2MSL TIME_WAIT状态等待的时间,具体时长取决于实现
TIME_WAIT和2MSL存在的意义,确保最后一次ACK应答被收到
半关闭 TCP连接中,A发送FIN请求,进入到FIN_WAIT_1状态,B端回应ACK后,(A端进入FIN_WAIT_2状态),B没立即使,发送FIN请求,此时就处于半关闭状态,此时A可以接受B发送的数据,但不能像A发送数据
1 2 3 4 5 6 7 8 #include <sys/socket.h> int shutdown (int sockfd, int how) ;
close和shutdown的区别,close只是减少描述符的引用计数,引用计数为0时才关闭连接,shutdown不考虑描述符的引用计数,直接关闭描述符
端口复用 用于解决TIME_WAIT期间,TCP连接没有完全断开之前不允许重新监听的问题
在server的TCP连接没有完全断开之前不允许重新监听是不合理的。因为,TCP连接没有完全断开指的是connfd(127.0.0.1:6666)没有完全断开,而我们重新监听的是lis-tenfd(0.0.0.0:6666),虽然是占用同一个端口,但IP地址不同,connfd对应的是与某个客户端通讯的一个具体的IP地址,而listenfd对应的是wildcard address。解决这个问题的方法是使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。
1 2 3 int opt = 1 ; setsockopt (listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt));
setsockopt和fcntl函数一样,用途很多,具体参考UNP第七章,这里只使用到他的端口复用功能
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 #include <sys/select.h> #include <sys/time.h> #include <sys/types.h> #include <unistd.h> int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) ; nfds: 监控的文件描述符集里最大文件描述符加1 ,因为此参数会告诉内核检测前多少个文件描述符的状态 readfds: 监控有读数据到达文件描述符集合,传入传出参数 writefds: 监控写数据到达文件描述符集合,传入传出参数 exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数 timeout: 定时阻塞监控时间,3 种情况 1. NULL ,永远等下去 2. 设置timeval,等待固定时间 3. 设置timeval里时间均为0 ,检查描述字后立即返回,轮询 struct timeval { long tv_sec; long tv_usec; }; 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 #include <string.h> #include <arpa/inet.h> #include <ctype.h> #include "wrap.h" #define SERV_PORT 8888 int main () { int i, j, n, maxi; int nready, client[FD_SETSIZE]; int maxfd, listenfd, connfd, sockfd; char buf[BUFSIZ], str[INET_ADDRSTRLEN]; struct sockaddr_in clie_addr, serv_addr; socklen_t cile_addr_len; fd_set rset, allset; listenfd = Socket (AF_INET, SOCK_STREAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERV_PORT); Bind (listenfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); Listen (listenfd, 20 ); maxfd = listenfd; maxi = -1 ; for (i = 0 ; i < FD_SETSIZE; i++) client[i] = -1 ; FD_ZERO (&allset); FD_SET (listenfd, &allset); while (1 ) { rset = allset; nready = select (maxfd+1 , &rset, NULL , NULL , NULL ); if (nready < 0 ) perr_exit ("select error" ); if (FD_ISSET (listenfd, &rset)) { cile_addr_len = sizeof (clie_addr); connfd = Accept (listenfd, (struct sockaddr*)&clie_addr, sizeof (clie_addr)); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &clie_addr.sin_addr, str, sizeof (str)), ntohs (clie_addr.sin_port)); for (i = 0 ; i < FD_SETSIZE; i++) if (client[i] < 0 ) { client[i] = connfd; break ; } if (i == FD_SETSIZE) { fputs ("too many clients\n" , stderr); exit (1 ); } FD_SET (connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; if (--nready == 0 ) continue ; } for (i = 0 ; i <= maxi; i++) { if ((sockfd = client[i]) < 0 ) continue ;; if (FD_ISSET (sockfd, &rset)) { if ((n = Read (sockfd, buf, sizeof (buf))) == 0 ) { Close (sockfd); FD_CLR (sockfd, &allset); client[i] = -1 ; } else if (n > 0 ) { for (j = 0 ; j < n; j++) buf[j] = toupper (buf[j]); sleep (10 ); Write (sockfd, buf, n); } if (--nready == 0 ) break ; } } } Close (listenfd); return 0 ; }
Poll poll和select的区别,select有上限1024,且除了重新编译内核之外没有办法修改,poll可以修改系统中的最大上限来更改。其次select中用于遍历文件描述符的数组,poll中变成了自带的,不需要再自定义了,其次select可以跨平台,poll针对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 #include <poll.h> int poll (struct pollfd *fds, nfds_t nfds, int timeout) ; struct pollfd { int fd; short events; short revents; }; POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND POLLRDNORM 数据可读 POLLRDBAND 优先级带数据可读 POLLPRI 高优先级可读数据 POLLOUT 普通或带外数据可写 POLLWRNORM 数据可写 POLLWRBAND 优先级带数据可写 POLLERR 发生错误 POLLHUP 发生挂起 POLLNVAL 描述字不是一个打开的文件 nfds 监控数组中有多少文件描述符需要被监控 timeout 毫秒级等待 -1 :阻塞等,#define INFTIM -1 Linux中没有定义此宏 0 :立即返回,不阻塞进程 >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 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <poll.h> #include <errno.h> #include <ctype.h> #include "wrap.h" #define MAXLINE 80 #define SERV_PORT 8888 #define OPEN_MAX 1024 int main () { int i, j, maxi, listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE], str[INET_ADDRSTRLEN]; socklen_t clilen; struct pollfd client[OPEN_MAX]; struct sockaddr_in cliaddr, servaddr; listenfd = Socket (AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt (listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero (&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (SERV_PORT); Bind (listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); Listen (listenfd, 128 ); client[0 ].fd = listenfd; client[0 ].events = POLLIN; for (i = 1 ; i < OPEN_MAX; i++) client[i].fd = -1 ; maxi = 0 ; for (;;) { nready = poll (client, maxi+1 , -1 ); if (client[0 ].revents & POLLIN) { clilen = sizeof (cliaddr); connfd = Accept (listenfd, (struct sockaddr*)&cliaddr, clilen); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs (cliaddr.sin_port)); for (i = 1 ; i < OPEN_MAX; i++) if (client[i].fd < 0 ) { client[i].fd = connfd; break ; } if (i == OPEN_MAX) perr_exit ("too many clients" ); client[i].events = POLLIN; if (i > maxi) maxi = i; if (--nready == 0 ) continue ;; } for (i =1 ;i <= maxi; i++) { if ((sockfd = client[i].fd) < 0 ) continue ; if (client[i].revents & POLLIN) { if ((n = Read (sockfd, buf, MAXLINE)) < 0 ) { if (errno == ECONNRESET) { printf ("client[%d] aborted connection\n" ,i); Close (sockfd); client[i].fd = -1 ; } else perr_exit ("read error" ); } else if (n == 0 ) { printf ("client[%d] closed connection\n" , i); Close (sockfd); client[i].fd = -1 ; } else { for (j = 0 ; j < n; j++) buf[j] = toupper (buf[j]); Writen (sockfd, buf, n); } if (--nready <= 0 ) break ; } } } return 0 ; }
Epoll poll跟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 100 101 102 103 104 105 106 107 108 109 110 111 #include <ctype.h> #include "wrap.h" #include <arpa/inet.h> #include <sys/epoll.h> #include <string.h> #define MAXLINE 8192 #define SERV_PORT 8888 #define OPEN_MAX 1024 int main () { int i, listenfd, connfd, sockfd; int n, num = 0 ; ssize_t nready, efd, res; char buf[MAXLINE], str[INET6_ADDRSTRLEN]; socklen_t clilen; struct sockaddr_in clien_addr, serv_addr; struct epoll_event tep, ep[OPEN_MAX]; listenfd = Socket (AF_INET, SOCK_STREAM, 0 ); int opt = 1 ; setsockopt (listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof (opt)); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERV_PORT); Bind (listenfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); Listen (listenfd, 20 ); efd = epoll_create (OPEN_MAX); if (efd == -1 ) perr_exit ("epoll_Create error" ); tep.events = EPOLLIN; tep.data.fd = listenfd; res = epoll_ctl (efd, EPOLL_CTL_ADD, listenfd, &tep); if (res == -1 ) perr_exit ("epoll_ctl error" ); for (;;) { nready = epoll_wait (efd, ep, OPEN_MAX, -1 ); if (nready == -1 ) perr_exit ("epoll_wait error" ); for (i = 0 ; i< nready; i++) { if (!(ep[i].events & EPOLLIN)) continue ; if (ep[i].data.fd == listenfd) { clilen = sizeof (clien_addr); connfd = Accept (listenfd, (struct sockaddr*)&clien_addr, &clilen); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &clien_addr.sin_addr, str, sizeof (str)), ntohs (clien_addr.sin_port)); printf ("cfd %d---client %d\n" , connfd, ++num); tep.events = EPOLLIN; tep.data.fd = connfd; res = epoll_ctl (efd, EPOLL_CTL_ADD, connfd, &tep); if (res == -1 ) perr_exit ("epoll_ctl error" ); } else { sockfd = ep[i].data.fd; n = Read (sockfd, buf, MAXLINE); if (n == 0 ) { res = epoll_ctl (efd, EPOLL_CTL_DEL, sockfd, NULL ); if (res == -1 ) perr_exit ("epoll_ctl error" ); Close (sockfd); printf ("client[%d] closed connection\n" , sockfd); } else if (n < 0 ) { perror ("read n < 0 error: " ); res = epoll_ctl (efd, EPOLL_CTL_DEL, sockfd, NULL ); Close (sockfd); } else { for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); Write (STDOUT_FILENO, buf, n); Writen (sockfd, buf, n); } } } } Close (listenfd); Close (efd); return 0 ; }
两种触发模式,边缘触发ET和水平触发LT,区别就是水平触发,在socket中有内容可读时,会反复调用epoll_wait触发然后继续读,直到读完,而边缘触发,只会调用读一次,读完继续阻塞到epoll_wait,等待下一个监听事件
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 #include <stdio.h> #include <stdlib.h> #include <sys/epoll.h> #include <errno.h> #include <unistd.h> #define MAXLINE 10 int main () { int efd, i; pid_t pid; int pfd[2 ]; char buf[MAXLINE], ch = 'a' ; pipe (pfd); pid = fork(); if (pid == 0 ) { close (pfd[0 ]); while (1 ) { for (i = 0 ; i < MAXLINE/2 ; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; for (;i < MAXLINE; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; write (pfd[1 ], buf, MAXLINE); sleep (3 ); } close (pfd[1 ]); } else if (pid > 0 ) { struct epoll_event event; struct epoll_event resevent[1 ]; int res, len; close (pfd[1 ]); efd = epoll_create (1 ); event.events = EPOLLIN | EPOLLET; event.data.fd = pfd[0 ]; epoll_ctl (efd, EPOLL_CTL_ADD, pfd[0 ], &event); while (1 ) { res = epoll_wait (efd, resevent, 1 , -1 ); printf ("res = %d\n" , res); if (resevent[0 ].data.fd == pfd[0 ]) { len = read (pfd[0 ], buf, MAXLINE/2 ); write (STDOUT_FILENO, buf, len); } } close (pfd[0 ]); close (efd); } else { perror ("fork error" ); exit (-1 ); } return 0 ; }
另一个借助socket实现的例子,上述例子是通过管道实现的,也就是epoll不仅仅只是适用于socket的情况下
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 #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <signal.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #define MAXLINE 10 #define SERV_PORT 7000 int main () { struct sockaddr_in serv_addr, cliaddr; socklen_t cliaddr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd; listenfd = socket (AF_INET, SOCK_STREAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERV_PORT); bind (listenfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); listen (listenfd, 20 ); struct epoll_event event; struct epoll_event resevent[10 ]; int res, len; efd = epoll_create (10 ); event.events = EPOLLIN | EPOLLET; printf ("Accepting connections ...\n" ); cliaddr_len = sizeof (cliaddr); connfd = accept (listenfd, (struct sockaddr*)&cliaddr, &cliaddr_len); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &cliaddr.sin_addr, str, sizeof (str)), ntohs (cliaddr.sin_port)); event.data.fd = connfd; epoll_ctl (efd, EPOLL_CTL_ADD, connfd, &event); while (1 ) { res = epoll_wait (efd, resevent, 10 , -1 ); printf ("res %d\n" , res); if (resevent[0 ].data.fd == connfd) { len = read (connfd, buf, MAXLINE/2 ); write (STDOUT_FILENO, buf, len); } } return 0 ; } #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #define MAXLINE 10 #define SERV_PORT 7000 int main () { struct sockaddr_in serv_addr; char buf[MAXLINE]; int sockfd, i; char ch = 'a' ; sockfd = socket (AF_INET, SOCK_STREAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; inet_pton (AF_INET, "127.0.0.1" , &serv_addr.sin_addr); serv_addr.sin_port = htons (SERV_PORT); connect (sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); while (1 ) { for (i = 0 ; i < MAXLINE/2 ; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; for (;i < MAXLINE; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; write (sockfd, buf, sizeof (buf)); sleep (5 ); } close (sockfd); return 0 ; }
非阻塞I/O方式,设置socket非阻塞,并且使用边沿触发ET模式,通常为我们选择的方式,ET能够减少epoll_wait的调用次数,通过while轮询,也能解决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 #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/wait.h> #include <sys/types.h> #include <sys/epoll.h> #include <unistd.h> #include <fcntl.h> #define MAXLINE 10 #define SERV_PORT 8888 int main () { struct sockaddr_in serv_addr, cli_addr; socklen_t cli_addr_len; int listenfd, connfd; char buf[MAXLINE]; char str[INET_ADDRSTRLEN]; int efd, flag; listenfd = socket (AF_INET, SOCK_STREAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERV_PORT); bind (listenfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); listen (listenfd, 20 ); struct epoll_event event; struct epoll_event resevent[10 ]; int res, len; efd = epoll_create (10 ); event.events = EPOLLIN | EPOLLET; printf ("Accepting connecton ...\n" ); cli_addr_len = sizeof (cli_addr); connfd = accept (listenfd, (struct sockaddr*)&cli_addr, &cli_addr_len); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &cli_addr.sin_addr, str, sizeof (str)), ntohs (cli_addr.sin_port)); flag = fcntl (connfd, F_GETFL); flag |= O_NONBLOCK; fcntl (connfd, F_SETFL, flag); event.data.fd = connfd; epoll_ctl (efd, EPOLL_CTL_ADD, connfd, &event); while (1 ) { printf ("epoll_wait begin\n" ); res = epoll_wait (efd, resevent, 10 , 1000 ); printf ("epoll_wait end res %d\n" , res); if (resevent[0 ].data.fd == connfd) { while ((len = read (connfd, buf, MAXLINE/2 )) > 0 ) write (STDOUT_FILENO, buf, len); } } return 0 ; } #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <netinet/in.h> #define MAXLINE 10 #define SERV_PORT 8888 int main () { struct sockaddr_in serv_addr; char buf[MAXLINE]; int sockfd, i; char ch = 'a' ; sockfd = socket (AF_INET, SOCK_STREAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; inet_pton (AF_INET, "127.0.0.1" , &serv_addr.sin_addr); serv_addr.sin_port = htons (SERV_PORT); connect (sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); while (1 ) { for (i = 0 ; i < MAXLINE/2 ; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; for (; i < MAXLINE; i++) buf[i] = ch; buf[i-1 ] = '\n' ; ch++; write (sockfd, buf, sizeof (buf)); sleep (5 ); } close (sockfd); return 0 ; }
epoll反应堆模型
与普通模型相比,多了一步重新设置监听事件,这样的作用,可以判断是否能向客户端写事件,因为客户端不一定能写,同时用到了epoll_event的arg参数,自定义了一个回调函数,可以看看一些比较好的代码思路
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 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 #include <stdio.h> #include <sys/socket.h> #include <sys/epoll.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <stdlib.h> #include <time.h> #define MAX_EVENTS 1024 #define BUFLEN 4096 #define SERV_PORT 8080 void recvdata (int fd, int events, void * arg) ;void senddata (int fd, int events, void * arg) ;struct my_events { int fd; int events; void * arg; void (*call_back)(int fd, int events, void * arg); int status; char buf[BUFLEN]; int len; long last_active; }; int g_efd; struct my_events g_events[MAX_EVENTS+1 ]; void eventset (struct my_events *ev, int fd, void (*call_back)(int , int , void *), void *arg) { ev->fd = fd; ev->call_back = call_back; ev->events = 0 ; ev->arg = arg; ev->status = 0 ; ev->last_active = time (NULL ); return ; } void eventadd (int efd, int events, struct my_events* ev) { struct epoll_event epv = {0 , {0 }}; int op; epv.data.ptr = ev; epv.events = ev->events = events; if (ev->status == 1 ) { op = EPOLL_CTL_MOD; } else { op = EPOLL_CTL_ADD; ev->status = 1 ; } if (epoll_ctl (efd, op, ev->fd, &epv) < 0 ) printf ("event add failed [fd=%d], events[%d]\n" , ev->fd, events); else printf ("event add OK [fd=%d], op =%d, events[%0X]\n" , ev->fd, op, events); return ; } void accpetconn (int lfd, int events, void * arg) { struct sockaddr_in cin; socklen_t len = sizeof (cin); int cfd, i; if ((cfd = accept (lfd, (struct sockaddr*)&cin, &len)) == -1 ) { if (errno != EAGAIN && errno != EINTR) { } printf ("%s:accept, %s\n" , __func__, strerror (errno)); return ; } do { for (i = 0 ; i < MAX_EVENTS; i++) if (g_events[i].status == 0 ) break ; if (i == MAX_EVENTS) { printf ("%s: max connect limit[%d]\n" , __func__, MAX_EVENTS); break ; } int flag = 0 ; if ((flag = fcntl (cfd, F_SETFL, O_NONBLOCK)) < 0 ) { printf ("%s: fcntl nonblocking failed, %s\n" , __func__, strerror (errno)); break ; } eventset (&g_events[i], cfd, recvdata, &g_events[i]); eventadd (g_efd, EPOLLIN, &g_events[i]); } while (0 ); printf ("new connect [%s:%d][time:%ld], pos[%d]\n" , inet_ntoa (cin.sin_addr), ntohs (cin.sin_port), g_events[i].last_active, i); return ; } void eventdel (int efd, struct my_events* ev) { printf ("------------------------------------------------------------------------waht ?" ); struct epoll_event epv = {0 , {0 }}; if (ev->status != 1 ) return ; epv.data.ptr = ev; ev->status = 0 ; epoll_ctl (efd, EPOLL_CTL_DEL, ev->fd, &epv); printf ("------------------------------------------------------------------------waht? after" ); return ; } void recvdata (int fd, int events, void * arg) { printf ("------------------------------------receive before\n" ); struct my_events * ev = (struct my_events*)arg; int len; printf ("------------------------------------len = recv before\n" ); len = recv (fd, ev->buf, sizeof (ev->buf), 0 ); printf ("------------------------------------len = recv after------------------------------------ len = %d\n" , len); eventdel (g_efd, ev); printf ("len = %d\n------------------------------------" , len); if (len > 0 ) { ev->len = len; ev->buf[len] = '\0' ; printf ("C[%d]:%s\n" , fd, ev->buf); eventset (ev, fd, senddata, ev); printf ("between set and add\n" ); eventadd (g_efd, EPOLLOUT, ev); printf ("------------------------------------what error?" ); } else if (len == 0 ) { close (ev->fd); printf ("[fd=%d] pos[%ld], closed\n" , fd, ev-g_events); } else { close (ev->fd); printf ("recv[fd=%d] error[%d]:%s\n" , fd, errno, strerror (errno)); } printf ("------------------------------------receive after\n" ); return ; } void senddata (int fd, int events, void * arg) { struct my_events * ev = (struct my_events*)arg; int len; len = send (fd, ev->buf, ev->len, 0 ); if (len > 0 ) { printf ("send[fd=%d] , [%d]%s\n" , fd, len, ev->buf); eventdel (g_efd, ev); eventset (ev, fd, recvdata, ev); eventadd (g_efd, EPOLLIN, ev); } else { close (ev->fd); eventdel (g_efd, ev); printf ("send[fd=%d] error %s\n" , fd, strerror (errno)); } return ; } void initlistensocket (int efd, short port) { int lfd = socket (AF_INET, SOCK_STREAM, 0 ); fcntl (lfd, F_SETFL, O_NONBLOCK); eventset (&g_events[MAX_EVENTS], lfd, accpetconn, &g_events[MAX_EVENTS]); eventadd (efd, EPOLLIN, &g_events[MAX_EVENTS]); struct sockaddr_in sin; memset (&sin, 0 , sizeof (sin)); sin.sin_family = AF_INET; sin.sin_addr.s_addr = INADDR_ANY; sin.sin_port = htons (SERV_PORT); bind (lfd, (struct sockaddr*)&sin, sizeof (sin)); listen (lfd, 20 ); return ; } int main (int argc, char * argv[]) { unsigned short port = SERV_PORT; if (argc == 2 ) port = atoi (argv[1 ]); g_efd = epoll_create (MAX_EVENTS+1 ); if (g_efd <= 0 ) printf ("create efd in %s err %s\n" , __func__, strerror (errno)); initlistensocket (g_efd, port); struct epoll_event evetns[MAX_EVENTS + 1 ]; printf ("server running:port[%d]\n" , port); int chekpos = 0 , i; while (1 ) { long now = time (NULL ); for (i = 0 ; i < 100 ; i++, chekpos++) { if (chekpos == MAX_EVENTS) chekpos = 0 ; if (g_events[chekpos].status != 1 ) continue ; long duration = now - g_events[chekpos].last_active; if (duration >= 5 ) { close (g_events[chekpos].fd); printf ("[fd=%d] timeout\n" , g_events[chekpos].fd); eventdel (g_efd, &g_events[chekpos]); } } printf ("------------------------------------epoll_wait before\n" ); int nfd = epoll_wait (g_efd, evetns, MAX_EVENTS+1 , 1000 ); if (nfd < 0 ) { printf ("epoll_wait error, exit\n" ); break ; } printf ("------------------------------------epoll_wait after\n" ); for (i = 0 ; i < nfd; i++) { struct my_events * ev = (struct my_events*)evetns[i].data.ptr; if ((evetns[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { ev->call_back (ev->fd, evetns[i].events, ev->arg); } if ((evetns[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { ev->call_back (ev->fd, evetns[i].events, ev->arg); } } } return 0 ; }
心跳包 在TCP网络通信中,经常会出现客户端和服务器之间的非正常断开,需要实时检测查询链接状态。常用的解决方法就是在程序中加入心跳机制。
Heart-Beat线程
这个是最常用的简单方法。在接收和发送数据时个人设计一个守护进程(线程),定时发送Heart-Beat包,客户端/服务器收到该小包后,立刻返回相应的包即可检测对方是否实时在线。
该方法的好处是通用,但缺点就是会改变现有的通讯协议!大家一般都是使用业务层心跳来处理,主要是灵活可控。
UNIX网络编程不推荐使用SO_KEEPALIVE来做心跳检测,还是在业务层以心跳包做检测比较好,也方便控制。
三种方式 1.心跳包 2.乒乓包(携带一些数据) 3.自带的机制(如下)
SO_KEEPALIVE 保持连接检测对方主机是否崩溃,避免(服务器)永远阻塞于TCP连接的输入。设置该选项后,如果2小时内在此套接口的任一方向都没有数据交换,TCP就自动给对方发一个保持存活探测分节(keepalive probe)。这是一个对方必须响应的TCP分节.它会导致以下三种情况:对方接收一切正常:以期望的ACK响应。2小时后,TCP将发出另一个探测分节。对方已崩溃且已重新启动:以RST响应。套接口的待处理错误被置为ECONNRESET,套接 口本身则被关闭。对方无任何响应:源自berkeley的TCP发送另外8个探测分节,相隔75秒一个,试图得到一个响应。在发出第一个探测分节11分钟 15秒后若仍无响应就放弃。套接口的待处理错误被置为ETIMEOUT,套接口本身则被关闭。如ICMP错误是“host unreachable(主机不可达)”,说明对方主机并没有崩溃,但是不可达,这种情况下待处理错误被置为EHOSTUNREACH。
线程池
把之前的一些芝士结合在一起了,不错
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 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 #ifndef __THREADPOOL_H_ #define __THREADPOOL_H_ typedef struct threadpool_t threadpool_t ;threadpool_t *threadpool_create (int min_thr_num, int max_thr_num, int queue_max_size) ;int threadpool_add (threadpool_t *pool, void *(*function)(void *arg), void *arg) ;int threadpool_destroy (threadpool_t *pool) ;int threadpool_all_threadnum (threadpool_t *pool) ;int threadpool_busy_threadnum (threadpool_t *pool) ;#endif #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <assert.h> #include <string.h> #include <stdio.h> #include <errno.h> #include <signal.h> #include "threadpool.h" #define DEFAULT_TIME 10 #define MIN_WAIT_TASK_NUM 10 #define DEFAULT_THREAD_VARY 10 #define true 1 #define false 0 typedef struct { void *(*function)(void *); void * arg; } threadpool_task_t ; struct threadpool_t { pthread_mutex_t lock; pthread_mutex_t thread_counter; pthread_cond_t queue_not_full; pthread_cond_t queue_not_empty; threadpool_task_t * task_queue; pthread_t * threads; pthread_t adjust_tid; int min_thr_num; int max_thr_num; int live_thr_num; int busy_thr_num; int wait_exit_thr_num; int queue_front; int queue_rear; int queue_size; int queue_max_size; int shutdown; }; void * threadpool_thread (void * threadpool) ;void * adjust_thread (void * threadpool) ;int is_thread_alive (pthread_t tid) ;int threadpool_free (threadpool_t * pool) ;threadpool_t * threadpool_create (int min_thr_num, int max_thr_num, int queue_max_size) { int i; threadpool_t * pool = NULL ; do { if ((pool = (threadpool_t *)malloc (sizeof (threadpool_t ))) == NULL ) { printf ("malloc threadpool fail" ); break ; } pool->min_thr_num = min_thr_num; pool->max_thr_num = max_thr_num; pool->busy_thr_num = 0 ; pool->live_thr_num = min_thr_num; pool->queue_size = 0 ; pool->queue_max_size = queue_max_size; pool->queue_front = 0 ; pool->queue_rear = 0 ; pool->shutdown = false ; pool->threads = (pthread_t *)malloc (sizeof (pthread_t )*max_thr_num); if (pool->threads == NULL ) { printf ("malloc threads fail" ); break ; } memset (pool->threads, 0 , sizeof (pthread_t )*max_thr_num); pool->task_queue = (threadpool_task_t *)malloc (sizeof (threadpool_task_t )*queue_max_size); if (pool->task_queue == NULL ) { printf ("malloc task_queue fail" ); break ; } if (pthread_mutex_init (&(pool->lock), NULL ) != 0 || pthread_mutex_init (&(pool->thread_counter), NULL ) != 0 || pthread_cond_init (&(pool->queue_not_empty), NULL ) != 0 || pthread_cond_init (&(pool->queue_not_full), NULL ) != 0 ) { printf ("init the lock or cond fail" ); break ; } for (i = 0 ; i < min_thr_num; i++) { pthread_create (&(pool->threads[i]), NULL , threadpool_thread, (void *)pool); printf ("start thread 0x%x...\n" , (unsigned )pool->threads[i]); } pthread_create (&(pool->adjust_tid), NULL , adjust_thread, (void *)pool); return pool; } while (0 ); threadpool_free (pool); return NULL ; } int threadpool_add (threadpool_t *pool, void *(*function)(void *arg), void *arg) { pthread_mutex_lock (&(pool->lock)); while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) { pthread_cond_wait (&(pool->queue_not_full), &(pool->lock)); } if (pool->shutdown) { pthread_mutex_unlock (&(pool->lock)); } if (pool->task_queue[pool->queue_rear].arg != NULL ) { free (pool->task_queue[pool->queue_rear].arg); pool->task_queue[pool->queue_rear].arg = NULL ; } pool->task_queue[pool->queue_rear].function = function; pool->task_queue[pool->queue_rear].arg = arg; pool->queue_rear = (pool->queue_rear + 1 ) % pool->queue_max_size; pool->queue_size++; pthread_cond_signal (&(pool->queue_not_empty)); pthread_mutex_unlock (&(pool->lock)); return 0 ; } void * threadpool_thread (void * threadpool) { threadpool_t * pool = (threadpool_t *)threadpool; threadpool_task_t task; while (true ) { pthread_mutex_lock (&(pool->lock)); while ((pool->queue_size == 0 ) && (!pool->shutdown)) { printf ("thread 0x%x is waiting\n" , (unsigned int )pthread_self ()); pthread_cond_wait (&(pool->queue_not_empty), &(pool->lock)); if (pool->wait_exit_thr_num > 0 ) { pool->wait_exit_thr_num--; if (pool->live_thr_num > pool->min_thr_num) { printf ("thread 0x%x is exiting\n" , (unsigned int )pthread_self ()); pool->live_thr_num--; pthread_mutex_unlock (&(pool->lock)); pthread_exit (NULL ); } } } if (pool->shutdown) { pthread_mutex_unlock (&(pool->lock)); printf ("thread 0x%x is exiting\n" , (unsigned int )pthread_self ()); pthread_exit (NULL ); } task.function = pool->task_queue[pool->queue_front].function; task.arg = pool->task_queue[pool->queue_front].arg; pool->queue_front = (pool->queue_front+1 ) % pool->queue_max_size; pthread_cond_broadcast (&(pool->queue_not_full)); pthread_mutex_unlock (&(pool->lock)); printf ("thread 0x%x start working\n" , (unsigned int )pthread_self ()); pthread_mutex_lock (&(pool->thread_counter)); pool->busy_thr_num++; pthread_mutex_unlock (&(pool->thread_counter)); (*(task.function))(task.arg); printf ("thread 0x%x end working\n" , (unsigned int )pthread_self ()); pthread_mutex_lock (&(pool->thread_counter)); pool->busy_thr_num--; pthread_mutex_unlock (&(pool->thread_counter)); } pthread_exit (NULL ); } void * adjust_thread (void * threadpool) { int i; threadpool_t * pool = (threadpool_t *)threadpool; while (!pool->shutdown) { sleep (DEFAULT_TIME); pthread_mutex_lock (&(pool->lock)); int queue_size = pool->queue_size; int live_thr_num = pool->live_thr_num; pthread_mutex_unlock (&(pool->lock)); pthread_mutex_lock (&pool->thread_counter); int busy_thr_num = pool->busy_thr_num; pthread_mutex_unlock (&(pool->thread_counter)); if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) { pthread_mutex_lock (&(pool->lock)); int add = 0 ; for (i = 0 ; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY && pool->live_thr_num < pool->max_thr_num; i++) { if (pool->threads[i] == 0 || !is_thread_alive (pool->threads[i])) { pthread_create (&(pool->threads[i]), NULL , threadpool_thread, (void *)pool); add++; pool->live_thr_num++; } } pthread_mutex_unlock (&(pool->lock)); } if ((busy_thr_num * 2 ) < live_thr_num && live_thr_num > pool->min_thr_num) { pthread_mutex_lock (&(pool->lock)); pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; pthread_mutex_unlock (&(pool->lock)); for (i = 0 ; i < DEFAULT_THREAD_VARY; i++) { pthread_cond_signal (&(pool->queue_not_empty)); } } } return NULL ; } int threadpool_destroy (threadpool_t *pool) { int i; if (pool == NULL ) { return -1 ; } pool->shutdown = true ; pthread_join (pool->adjust_tid, NULL ); for (i = 0 ; i < pool->live_thr_num; i++) { pthread_cond_broadcast (&(pool->queue_not_empty)); } for (i = 0 ; i< pool->live_thr_num; i++) { pthread_join (pool->threads[i], NULL ); } threadpool_free (pool); return 0 ; } int threadpool_free (threadpool_t * pool) { if (pool == NULL ) { return -1 ; } if (pool->task_queue) { free (pool->task_queue); } if (pool->threads) { free (pool->threads); pthread_mutex_lock (&(pool->lock)); pthread_mutex_destroy (&(pool->lock)); pthread_mutex_lock (&(pool->thread_counter)); pthread_mutex_destroy (&(pool->thread_counter)); pthread_cond_destroy (&(pool->queue_not_empty)); pthread_cond_destroy (&(pool->queue_not_full)); } free (pool); pool = NULL ; return 0 ; } int threadpool_all_threadnum (threadpool_t *pool) { int all_threadnum = -1 ; pthread_mutex_lock (&(pool->lock)); all_threadnum = pool->live_thr_num; pthread_mutex_unlock (&(pool->lock)); return all_threadnum; } int threadpool_busy_threadnum (threadpool_t *pool) { int busy_threadnum = -1 ; pthread_mutex_lock (&(pool->thread_counter)); busy_threadnum = pool->busy_thr_num; pthread_mutex_unlock (&(pool->thread_counter)); return busy_threadnum; } int is_thread_alive (pthread_t tid) { int kill_rc = pthread_kill (tid, 0 ); if (kill_rc == ESRCH) { return false ; } return true ; } void * process (void * arg) { printf ("thread 0x%x working on task %d\n" , (unsigned int )pthread_self (), *(int *)arg); sleep (1 ); printf ("task %d is end\n" , *(int *)arg); return NULL ; } int main (void ) { threadpool_t * thp = threadpool_create (3 ,100 ,100 ); printf ("pool inited" ); int num[20 ], i; for (i = 0 ; i < 20 ; i++) { num[i] = i; printf ("add task %d\n" , i); threadpool_add (thp, process, (void *)&num[i]); } sleep (10 ); return 0 ; }
UDP 与TCP相比,TCP为流式协议,UDP为报式协议
TCP:优势:1.数据稳定(丢包回传) 2.流率稳定 3.流量稳定(滑动窗口)
劣势 效率低,速度慢
UDP: 优势 效率高,速度快
劣势 数据,流率,流量不稳定
UDP为了防止丢包,可以通过改变缓冲区大小,
1 2 3 4 #include <sys/socket.h> int setsockopt (int sockfd, int level, int optname, const void * optval, socklen_t optlen) ;int n = 220 x1024 setsockopt (sockfd, SOL_SOCKET, SO_RCVBUF, &N, sizeof (n));
UDP C/S模型 和TCP的区别,不需要accpet和客户端连接了,直接读就行了,所以也可以引出另一个特点,UDP通信自带多线程/多进程了,不需要向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 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 #include <string.h> #include <stdio.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main () { struct sockaddr_in serv_addr, clie_addr; socklen_t clie_addr_len; int sockfd; char buf[BUFSIZ]; char str[INET_ADDRSTRLEN]; int i, n; sockfd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERV_PORT); bind (sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); printf ("Accepting connections...\n" ); while (1 ) { clie_addr_len = sizeof (clie_addr); n = recvfrom (sockfd, buf, BUFSIZ, 0 , (struct sockaddr*)&clie_addr, &clie_addr_len); if (n == -1 ) perror ("Recvfrom error" ); printf ("received from %s at PORT %d\n" , inet_ntop (AF_INET, &clie_addr.sin_addr, str, sizeof (str)), ntohs (clie_addr.sin_port)); for (i = 0 ; i < n; i++) buf[i] = toupper (buf[i]); n = sendto (sockfd, buf, n, 0 , (struct sockaddr*)&clie_addr, sizeof (clie_addr)); if (n == -1 ) perror ("sendto error" ); } close (sockfd); return 0 ; } #include <stdio.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <ctype.h> #define SERV_PORT 8000 int main () { struct sockaddr_in serv_addr; int sockfd, n; char buf[BUFSIZ]; sockfd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; inet_pton (AF_INET, "127.0.0.1" , &serv_addr.sin_addr); serv_addr.sin_port = htons (SERV_PORT); while (fgets (buf, BUFSIZ, stdin) != NULL ) { n = sendto (sockfd, buf, strlen (buf), 0 , (struct sockaddr*)&serv_addr, sizeof (serv_addr)); if (n == -1 ) perror ("error" ); n = recvfrom (sockfd, buf, BUFSIZ, 0 , NULL , 0 ); if (n == -1 ) perror ("error" ); write (STDOUT_FILENO, buf, n); } close (sockfd); return 0 ; }
广播 通过特殊的IP地址 当前网段.255(xxx.xxx.xxx.255),同时需要借助setsockopt函数更改socket的选项(允许发送广播数据报)
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 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/socket.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define MAXLINE 1500 #define BROADCAST_IP "172.23.175.255" #define CLIENT_PORT 9000 int main () { int sockfd; struct sockaddr_in serv_addr, clie_addr; char buf[MAXLINE]; sockfd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERVER_PORT); bind (sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); int flag = 1 ; setsockopt (sockfd, SOL_SOCKET, SO_BROADCAST, &flag, sizeof (flag)); bzero (&clie_addr, sizeof (clie_addr)); clie_addr.sin_family = AF_INET; inet_pton (AF_INET, BROADCAST_IP, &clie_addr.sin_addr.s_addr); clie_addr.sin_port = htons (CLIENT_PORT); int i = 0 ; while (1 ) { sprintf (buf, "Drink %d glasses of water\n" , i++); sendto (sockfd, buf, strlen (buf), 0 , (struct sockaddr*)&clie_addr, sizeof (clie_addr)); sleep (1 ); } close (sockfd); return 0 ; } #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/socket.h> #include <arpa/inet.h> #define SERVER_PORT 8000 #define MAXLINE 4096 #define CLIENT_PORT 9000 int main () { struct sockaddr_in localaddr; int confd; ssize_t len; char buf[MAXLINE]; confd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton (AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons (CLIENT_PORT); int ret = bind (confd, (struct sockaddr*)&localaddr, sizeof (localaddr)); if (ret == 0 ) printf ("...bind ok...\n" ); while (1 ) { len = recvfrom (confd, buf, sizeof (buf), 0 , NULL , 0 ); write (STDOUT_FILENO, buf, len); } close (confd); return 0 ; }
组播 组播组可以是永久的也可以是临时的。组播组地址中,有一部分由官方分配的,称为永久组播组。永久组播组保持不变的是它的ip地址,组中的成员构成可以发生变化。永久组播组中成员的数量都可以是任意的,甚至可以为零。那些没有保留下来供永久组播组使用的ip组播地址,可以被临时组播组利用。
224.0.0.0~224.0.0.255 为预留的组播地址(永久组地址),地址224.0.0.0保留不做分配,其它地址供路由协议使用;
224.0.1.0~224.0.1.255 是公用组播地址,可以用于Internet;欲使用需申请。
224.0.2.0~238.255.255.255 为用户可用的组播地址(临时组地址),全网范围内有效;
239.0.0.0~239.255.255.255 为本地管理组播地址,仅在特定的本地范围内有效。
使用ip ad命令查看网卡编号
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 itcast$ ip ad 1 : lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default link/loopback 00 :00 :00 :00 :00 :00 brd 00 :00 :00 :00 :00 :00 inet 127.0 .0 .1 /8 scope host lo valid_lft forever preferred_lft forever inet6 ::1 /128 scope host valid_lft forever preferred_lft forever 2 : eth0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 link/ether 00 :0 c:29 :0 a:c4:f4 brd ff:ff:ff:ff:ff:ff inet6 fe80::20 c:29f f:fe0a:c4f4/64 scope link valid_lft forever preferred_lft forever 1 : lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00 :00 :00 :00 :00 :00 brd 00 :00 :00 :00 :00 :00 inet 127.0 .0 .1 /8 scope host lo valid_lft forever preferred_lft forever inet6 ::1 /128 scope host valid_lft forever preferred_lft forever 2 : bond0: <BROADCAST,MULTICAST,MASTER> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 1 a:fc:2 a:2 e:5 a:28 brd ff:ff:ff:ff:ff:ff 3 : dummy0: <BROADCAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether ca:9 d:9f :9b :96 :7f brd ff:ff:ff:ff:ff:ff 4 : eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00 :15 :5 d:6 e:db:0 c brd ff:ff:ff:ff:ff:ff inet 172.23 .164 .160 /20 brd 172.23 .175 .255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::215 :5 dff:fe6e:db0c/64 scope link valid_lft forever preferred_lft forever 5 : tunl0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/ipip 0.0 .0 .0 brd 0.0 .0 .0 6 : sit0@NONE: <NOARP> mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0 .0 .0 brd 0.0 .0 .0
代码部分和广播有点像,但有一点区别,首先同样也得开发组播权限通过setsockopt ,其次服务器端和客户端有一定区别,服务器端是向某组播地址广播,客户端是加入某组播地址
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 #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define CLIENT_PORT 9000 #define MAXLINE 1500 #define GROUP "239.0.0.2" int main () { int sockfd; struct sockaddr_in serv_addr, clie_addr; char buf[MAXLINE] = "itcast\n" ; struct ip_mreqn group; sockfd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&serv_addr, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl (INADDR_ANY); serv_addr.sin_port = htons (SERVER_PORT); bind (sockfd, (struct sockaddr*)&serv_addr, sizeof (serv_addr)); inet_pton (AF_INET, GROUP, &group.imr_multiaddr); inet_pton (AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt (sockfd, IPPROTO_IP, IP_MULTICAST_IF, &group, sizeof (group)); bzero (&clie_addr, sizeof (clie_addr)); clie_addr.sin_family = AF_INET; inet_pton (AF_INET, GROUP, &clie_addr.sin_addr.s_addr); clie_addr.sin_port = htons (CLIENT_PORT); int i = 0 ; while (1 ) { sprintf (buf, "itcast %d\n" , i++); sendto (sockfd, buf, strlen (buf), 0 ,(struct sockaddr*)&clie_addr, sizeof (clie_addr)); sleep (1 ); } close (sockfd); return 0 ; } #include <stdio.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> #include <net/if.h> #define SERVER_PORT 8000 #define CLIENT_PORT 9000 #define GROUP "239.0.0.2" int main () { struct sockaddr_in localaddr; int connfd; ssize_t len; char buf[BUFSIZ]; struct ip_mreqn group; connfd = socket (AF_INET, SOCK_DGRAM, 0 ); bzero (&localaddr, sizeof (localaddr)); localaddr.sin_family = AF_INET; inet_pton (AF_INET, "0.0.0.0" , &localaddr.sin_addr.s_addr); localaddr.sin_port = htons (CLIENT_PORT); bind (connfd, (struct sockaddr*)&localaddr, sizeof (localaddr)); inet_pton (AF_INET, GROUP, &group.imr_multiaddr); inet_pton (AF_INET, "0.0.0.0" , &group.imr_address); group.imr_ifindex = if_nametoindex("eth0" ); setsockopt (connfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &group, sizeof (group)); while (1 ) { len = recvfrom (connfd, buf, sizeof (buf), 0 , NULL , 0 ); write (STDOUT_FILENO, buf, len); } close (connfd); return 0 ; }
setsockopt 总结目前用过的该函数的功能
端口复用
设置缓冲区大小(udp开头)
开放广播权限
开放组播权限
加入组播组
UNP书籍部分笔记 第1章 简介和TCP/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 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 #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <ctype.h> #define MAXLINE 4096 #define LISTENQ 1024 int main (int argc, char **argv) { int sockfd, n, counter = 0 ; char recvline[MAXLINE + 1 ]; struct sockaddr_in servaddr; if (argc != 2 ) perror ("usage: a.out <IPaddress>" ); if ( (sockfd = socket (AF_INET, SOCK_STREAM, 0 )) < 0 ) perror ("socket error" ); bzero (&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons (6666 ); if (inet_pton (AF_INET, argv[1 ], &servaddr.sin_addr) <= 0 ) perror ("inet_pton error for" ); if (connect (sockfd, (struct sockaddr*) &servaddr, sizeof (servaddr)) < 0 ) perror ("connect error" ); while ( (n = read (sockfd, recvline, MAXLINE)) > 0 ) { counter++; recvline[n] = 0 ; if (fputs (recvline, stdout) == EOF) perror ("fputs error" ); } if (n < 0 ) perror ("read error" ); printf ("counter=%d\n" , counter); exit (0 ); } #include <stdio.h> #include <unistd.h> #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #define MAXLINE 4096 #define LISTENQ 1024 int main (int argc, char **argv) { int listenfd, connfd, i; struct sockaddr_in servaddr; char buff[MAXLINE]; time_t ticks; listenfd = socket (AF_INET, SOCK_STREAM, 0 ); bzero (&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl (INADDR_ANY); servaddr.sin_port = htons (6666 ); bind (listenfd, (struct sockaadr*) &servaddr, sizeof (servaddr)); listen (listenfd, LISTENQ); for ( ; ; ) { connfd = accept (listenfd, (struct sockaddr *) NULL , NULL ); ticks = time (NULL ); snprintf (buff, sizeof (buff), "%.24s\r\n" , ctime (&ticks)); for (i=0 ; i<strlen (buff); i++) write (connfd, &buff[i], 1 ); close (connfd); } }
尽管服务器端,write时分开26次write,但客户端counter仍然只会累计一次,其结果随客户主机和服务器主机而定。如果客户端和服务器运行在同一台主机上,就如同上述情况(7.9节,就Nagle算法讨论解释如此行为的原因)
第2章 传输层TCP/UDP 重点为分层模型,三次握手和四次握手,以及TCP状态转换,TCP/UDP协议格式,TIME_WAIT状态
小结
第3章 套接口简介 3.1 概述 本章主要内容,套接口结构,主要分为IPV4和IPV6两种格式,以及主机字节序到网络字节序的一系列函数,开发了更好的字节操纵函数,以及与协议无关的一套套接口函数
3.2 套接口结构(sockaadr_in,sockaddr) IPV4套接口地址结构
1 2 3 4 5 6 7 8 9 10 11 struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { uint8_t sin_len; sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; char sin_zero[8 ]; };
sin_addr成员由于历史原因是一个结构,早期定义为不同结构的联合。
POSIX规范要求的数据类型
通用套接口地址结构
1 2 3 4 5 6 struct sockaddr { uint8_t sa_len; sa_family_t sa_family; char sa_data[14 ]; };
套接口函数被定义为采用指向通用套接口地址结构的指针,如bind函数的原型
1 int bind (int , struct sockaddr*, socklen_t ) ;
其实可以更好的采用void来实现,最终为sockaddr 同样因为历史原因,套接口函数的定义早于void*
IPV6套接口地址结构
1 2 3 4 5 6 7 8 9 10 11 12 13 struct in6_addr { uint8_t s6_addr[16 ]; }; #define SIN6_LEN struct sockaddr_int6 { uint8_t sin6_len; uint8_t sin6_family; sa_family_t sin6_port; uint32_t sin6_flowinfo; struct in6_addr sin6_addr ; uint32_t sin6_scope_id; };
具体成员说面,见书
新的通用套接口地址结构
1 2 3 4 5 6 7 struct sockaddr_storage { uint8_t ss_len; sa_family_t ss_family; };
与原先的通用结构的差别:
如果系统支持的任何套接口地址结构又对齐需求,那么sockaddr_storage能够满足最苛刻的对齐要求
sockaddr_storage足够大,能够容纳系统支持的任何套接口地址结构
套接口地址结构的比较
看图即可
3.3 值结果参数 简单来说,函数的定义传进时作为值,给函数提供条件,结束时将结果返回给这个参数,所以这个参数可以是指针类型,能够将函数内改动的结果带出来
1 2 3 4 5 6 7 struct sockaddr_un cil ; socklen_t len;len = sizeof (cil); getpeername(unixfd, (sturct sockaddr*)&cil, &len);
3.4 字节排序函数(htons,htonl,ntohs,ntols) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void test_big_or_little_endian () { union { short s; char c[sizeof (short )]; } fn; fn.s = 0x0102 ; if (sizeof (short == 2 )) { if (fn.c[0 ] == 1 && fn.c[1 ] == 2 ) printf ("big-endian\n" ); else if (fn.c[0 ] == 2 && fn.c[1 ] == 1 ) printf ("little-endian\n" ); else printf ("unknown\n" ); } else printf ("sizeof(short) = %d\n" , sizeof (short )); }
由于这两种字节序没有标准,且都有系统使用,而网络协议在处理这些多字节整数时,使用大端字节序,所以需要一系列转换函数
1 2 3 4 5 6 7 #include <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.5 字节操纵函数(bzero,memset….) 就memset和bzero那一系列,主要用到的就bzero,其他用到的时候再看
1 2 3 4 5 6 7 8 9 10 11 #include <strings.h> void bzero (void * dest, size_t nbytes) ;void bcopy (const void * src, void * dest, size_t nbytes) ;int bcmp (const void * ptr1, cosnt void * ptr2, size_t nbytes) ; #include <string.h> void * memset (void * dest, int c, size_t len) ;void * memcpy (void * dest, const void * src, size_t nbytest) ;int memcmp (const void * ptr1, const void * ptr2, size_t nbytes) ;
3.6 inet_aton, inet_addr, inet_ntoa函数
inet_aton,inet_addr,inet_ntoa在点分十进制数串(如:”206.168.112.96”)与它的网络字节序二进制值间转换IPV4地址
两个较新的函数:inet_pton和inet_ntop对IPV4和IPV6都能处理
1 2 3 4 5 6 7 #include <arpa/inet.h> int inet_aton (const char * strptr, struct in_addr* addrptr) ; in_addr_t inet_addr (const char * strptr) ; char * inet_ntoa (struct in_addr inaddr) ;
inet_addr可能出现的一个问题,出错时返回的INADDR_NONE 其值为一个32位均为1的值,也就意味着255.255.255.255不能由该函数处理,它的二进制值与INADDR_NONE一样
3.7 inet_pton和inet_ntop函数 1 2 3 4 5 #include <arpa/inet.h> int inet_pton (int family, const char * strptr, void * addrptr) ; const char * inet_ntop (int family, const void * addrptr, char * strptr, size_t len) ;
两函数均以参数作为结果的返回值,pton以addrptr为转换后的结果,ntop以strptr为储存的结果,所以strptr不能时空指针,len的大小,为有助于规定这个大小,在**<netinet/in.h>**中有如下定义
1 2 #define INET_ADDRSTRLEN 16 #define INET6_ADDRSTRLEN 46
所有转换函数图示一览
1 2 3 4 5 6 7 8 9 10 inet_pton (AF_INET, cp, &foo.sin_addr);foo.sin.addr_s_addr = inet_addr (cp); char str[INET_ADDRSTRLEN];ptr = inet_ntop (AF_INET, &foo.sin_addr, str, sizeof (str)); ptr = inet_ntoa (foo.sin_addr);
3.8 sock_ntop和相关函数 对于一般的ntop和pton函数来说,我们必须知道结构的地址族,sock_ntop及其他一系列函数,则将这种情况避免,实现协议无关的套接字函数,如下为其一的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "unp.h" char * sock_ntop (const struct sockaddr* sockaddr, socklen_t addrlen) ; char * sock_ntop (const struct sockaddr* sa, socklen_t salen) { char portstr[8 ]; static char str[128 ]; switch (sa->sa_family) { case AF_INET: { struct sockaddr_in * sin = (struct sockaddr_in*)sa; if (inet_ntop (AF_INET, &sin->sin_addr, str, sizeof (str) NULL ) return NULL ; if (ntohs (sin->sinport) != 0 ) { snprintf (portstr, sizeof (portstr), ":%d" , ntohs (sin->sin_port)); strcat (str, portstr); } return (str); } } }
其它的
3.9 readn, writen 和 readline函数 同样是本书开发的一系列函数,简单来说就是,不回出现read或write由于某些影响没有执行完的情况,这些函数遇到这种情况后,会再次调用,也就是不需要使用者操心这些没有执行完的情况了
1 2 3 4 5 ssize_t readn (int filedes, void * buff, size_t nbytes) ;ssize_t writen (int filedes, const void * buff, size_t nbytes) ;ssize_t readline (int filedes, void * buff, size_t maxlen) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ssize_t readn (int fd, void * vptr, size_t n) { size_t nleft; ssize_t nread; char * ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ((nread = read (fd, ptr, nleft)) < 0 ) { if (errno == EINTR) nread = 0 ; else return -1 ; } else if (nread == 0 ) break ; nleft -= nread; ptr += nread; } return (n-nleft); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ssize_t writen (int fd, const void * vptr, size_t n) { size_t nleft; ssize_t nwritten; const char * ptr; ptr = vptr; nleft = n; while (nleft > 0 ) { if ((nwritten = write(fd, ptr, nleft)) <= 0 ) { if (nwritten < 0 && errno == EINTR) nwritten = 0 ; else return -1 ; } nleft -= nwritten; ptr += nwritten; } return n; }
1 2 3 4 5 6 7 8 9 10 11 12 13 ssize_t readline (int fd, void * vptr, size_t maxlen) { ssize_t n, rc; char c, *ptr; ptr = vptr; for (n = 1 ; n < maxlen; n++) { again: if ((rc = read(fd, &c, 1 )) == 1 ) { *ptr+ } } }
3.10 小结
疑:
为什么诸如套接口地址结构的长度这样的值-结果参数要用指针来传递?
为什么函数readn和writen都将void型指针转换为char 型指针
1:只有这样才能将改变后的值传出函数
2:指针增长需按所读或所写的字节增长,void由于不知道所指类型,不知道如何增长
第4章 基本TCP套接口 4.1 概述 基本的接口,socket,bind,connect,listen,accept
4.2 socket函数 1 2 3 #include <sys/socket.h> int socket (int family, int type, int protocol) ;
family(图4.2)
type(图4.3)
protocol(图4.4)
通常来说,family为AF_INET或AF_INET6,type根据TCP还是UDP分别设置为SOCK_STREAM或SOCK_DGRAM,protocol设置为0,选择缺省值
4.3 connect函数 TCP客户用connect函数来建立与TCP服务器的连接
1 2 3 #include <sys/socket.h> int connect (int sockfd, const struct sockaddr* servaddr, socklen_t addrlen) ;
sockfd是由socket函数返回的套接口描述字,第二第三参数分别是一个指向套接口地址的结构的指针和该结构的大小。客户在调用函数connect前不必非得调用bind函数,需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。如果是TCP套接口,调用connect函数将激发TCP的三路握手过程(2.6),并且仅在连接建立成功或出错时菜返回
4.4 bind函数 bind函数把一个本地协议地址赋予一个套接口,对于网际协议,协议地址是32位的IPV4地址或128位的IPV6地址与16位的TCP或UDP端口号的组合
1 2 3 #include <sys/socket.h> int bind (int sockfd, const struct sockaddr* myaddr, socklen_t addrlen) ;
指定端口号为0,或指定地址值为INADDR_ANY则由内核选择端口号或指定IP地址
1 2 3 4 5 6 struct sockaddr_in servaddr ;servaddr.sin_addr.s_addr = htonl(INADDR_ANY); struct sockaddr_in6 serv ;serv.sin6_addr = in6addr_any;
如果让内核来选择临时值,由于第二参数有const修饰,它无法返回所选之值。为了得到内核所选择的这个临时端口值,必须调用函数 getsockname 来返回协议地址
4.5 listen函数 listen函数仅由TCP服务器调用,它做两件事
当socket函数创建一个套接口时,它被假设为一个主动套接口,也就是说,他是一个将调用connect发起连接的客户套接口。listen函数把一个未连接的套接口,转换成一个被动套接口。由TCP状态转换图,调用listen导致套接口从CLOSED状态转换到LISTEN状态
该函数的第二个参数规定了内核应该为相应套接口排队的最大连接个数
1 2 3 #include <sys/socket.h> int listen (int sockfd, int backlog) ;
4.6 accept函数 accpet函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠
1 2 3 #include <sys/socket.h> int accept (int sockfd, struct sockaddr* cliaddr, soclen_t * addrlen) ;
参数cliaddr,addrlen用来返回对端进程的协议地址,如果不需要这两个参数,可以置为NULL
1 2 3 4 5 6 7 8 9 10 11 12 ... for (;;) { len = sizeof (cliaddr); connfd = accept (listenfd, (struct sockaddr*)&cliaddr, &len); printf ("connection from %s. port %d\n" , Inet_ntop (AF_INET, &cliaddr.sin_addr, buff, sizeof (buff)), ntohs (cliaddr.sin_port)); ... }
4.7 fork和exec函数 1 2 3 #include <unistd.h> pid_t fork (void ) ;
详细的参考APUE note
进程在调用exec之前打开着的描述字通常跨exec继续保持打开,但可以通过fcntl设置FD_CLOEXEC描述字标志禁止掉
4.8 并发服务器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 pid_t pid;int listenfd, connfd;listenfd = Socket(...); Bind(listenfd, ...); Listen(listenfd, LISTENQ); for (;;) { connfd = Accept(listenfd, ...); if ((pid = Fork()) == 0 ) { Close(listenfd); doit(connfd); Close(connfd); exit (0 ); } Close(connfd); }
为什么close两次,因为每个文件或套接口都有一次引用计数,而fork返回后,描述字在父子进程间共享,因此着两个套接口的计数为2
4.9 close函数 Unix通常的close函数也用来关闭套接口,并终止TCP连接
1 2 3 #include <unistd.h> int close (int sockfd) ;
close实际上是引用计数减一并不会发送FIN,只有在计数为0后,才能达到我们想要的效果,如果想在某个TCP连接发送一个FIN,可以改用shutdown函数(6.6)
4.10 getsockname和getpeername函数 这两个函数返回与某个套接口关联的本体协议地址,或者返回与某个套接口关联的远地协议地址
1 2 3 4 #include <sys/socket.h> int getsockname (int sockfd, struct sockaddr* localaddr, socklen_t * addrlen) ;int getpeername (int sockfd, struct sockaddr* peeraddr, socklen_t * addrlen) ;
1 2 3 4 5 6 7 8 9 10 int sockfd_to_family (int sockfd) { struct sockaddr_storage ss ; socklen_t len; len = sizeof (ss); if (getsockname(sockfd, (struct sockaddr*)&ss, &len) < 0 ) return -1 ; return (ss.ss_family); }
4.11 小结 所有客户和服务器都从调用socket开始,它返回一个套接字描述字。客户随后调用connect,服务器则调用bind,listen和accept 。套接口通常使用标准close函数关闭,不过将看到使用shutdown函数关闭套接口的另一种方法(6.6),还将查看SO_LINGER套接口选项对于关闭套接口的效果
疑:
1.图4.11中,如果把服务器程序中的listen调用函数,会发生什么。
accept返回EINVAL,因为它的第1个参数不是一个监听套接口描述字
第5章 TCP客户/服务器程序例子 5.1 概述 一个简单的cs模型的回射服务器
5.2 TCP回射服务器 5.3 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 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 #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 #define MAXLINE 1024 void str_echo (int connfd) ;int main (int argc, char ** argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; char clie_IP[MAXLINE]; struct sockaddr_in cliaddr , servaddr ; if ((listenfd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) perror("socket error" ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) perror("bind error" ); if (listen(listenfd, 20 ) < 0 ) perror("listen error" ); printf ("ready for accept\n" ); for (;;) { clilen = sizeof (cliaddr); if ((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0 ) perror("accept error" ); printf ("connect success form,client IP:**, client port:%d\n" , ntohs(cliaddr.sin_port)); if ((childpid = fork()) == 0 ) { close(listenfd); str_echo(connfd); exit (0 ); } close(connfd); } } void str_echo (int connfd) { ssize_t n; char buf[MAXLINE]; again: while ((n = read(connfd, buf, MAXLINE)) > 0 ) { write(connfd, buf, n); write(STDOUT_FILENO, buf, n); } if (n < 0 && errno == EINTR) goto again; else if (n < 0 ) perror("str_echo: read error" ); }
5.4 TCP回射客户端 5.5 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 46 47 48 49 50 51 52 53 54 #include <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <string.h> #include <stdlib.h> #include <stdio.h> #include <arpa/inet.h> #define MAXLINE 1024 #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 void str_cli (FILE* fp, int sockfd) ;int main (int argc, char ** argv) { char buf[8 ]; strcpy (buf, "hello" ); int sockfd; struct sockaddr_in servaddr ; if ((sockfd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) perror("socket error" ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &servaddr.sin_addr.s_addr); if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) perror("connect error" ); str_cli(stdin , sockfd); exit (0 ); } void str_cli (FILE* fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLINE]; int n; while (fgets(sendline, MAXLINE, fp) != NULL ) { write(sockfd, sendline, strlen (sendline)); if ((n = read(sockfd, recvline, MAXLINE) )== -1 ) perror("read error" ); write(STDOUT_FILENO, recvline, n); } }
5.6 正常启动 5.7 正常终止 5.8 POSIX信号处理 信号部分包括后面的wait处理子进程,AUPE有详细的描述,这边简单描述一下
5.9 SIGCHLD 子进程终止时,将SIGCHLD信号发送给父进程,此时可以调用wait / waitpid 清理僵尸进程
1 2 3 4 5 6 7 8 9 void sig_chld (int signo) { pid_t pid; int stat; pid = wait(&stat); printf ("child %d terminated\n" , pid); return ; }
5.10 wait和waitpid函数 1 2 3 4 #include <sys/wait.h> pid_t wait (int * statloc) ;pid_t waitpid (pid_t pid, int * statloc, int options) ;
详细的区别见AUPE
1 2 3 4 5 6 7 8 9 10 void sig_chld (int signo) { pid_t pid; int stat; while ((pid = waitpid(-1 , &stat, WNOHANG)) > 0 ) printf ("child %d terminated\n" , pid); return ; }
WNOHANG设置为非阻塞,它告知waitpid在有尚未终止子进程在运行时不要阻塞
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 int main (int argc, char ** argv) { int listenfd, connfd; pid_t childpid; socklen_t clilen; struct sockaddr_in cliaddr , servaddr ; void sig_chld (int ) ; listenfd = Socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servadd.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); Bind(listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); Listen(listenfd, LISTENQ); Signal(SIGCHLD, sig_chld); for (;;) { clilen = sizeof (cliaddr); if ((connfd = accept(listenfd, (strucr sockaddr*)&cliaddr, &clilen)) < 0 ) { if (errno == EINTR) continue ; else err_sys("accept error" ); } if ((childpid = Fork()) == 0 ) { Close(listenfd); str_echo(connfd); } close(connfd); } }
本节示范的三个目的:
1.当fork子进程时,必须捕获SIGCHLD信号
2.当捕获信号时,必须处理被中断的系统调用,如父进程的ACCEPT可能被 SIGCHLD的处理函数中断,所以需要放在循环内
3.SIGCHLD处理函数的正确编写,使用waitpid循环处理
5.11 accept返回前连接夭折
如何模拟改方法,TCP三路握手完成后,客户却发送一个RST(复位)
启动服务器,让它调用socket,bind和listen, 在调用accept之前睡眠一小段时间。在服务器进程睡眠时, 启动客户,让它调用socket和connect。一旦connect返回,就设置SO_LINGER套接口选项以产生一个RST
5.12 服务器进程终止
当我们杀死服务器子进程时,SIGCHLD信号被发送给服务器父进程,然后得到处理。客户上没有发生任何特殊之事。客户TCP接受来自服务器TCP的FIN并响应以一个ACK,然而问题是客户进程阻塞在fgets调用上,等待终端接收一行文本 , 此时netstat -a 查看套接口状态
当我们键入 another line 时,客户TCP把数据发送给服务器。TCP允许这么做,客户TCP收到FIN只是表示服务器进程已关闭了连接的服务器端,从而不再往其中发送数据。FIN的接受没有告知客户TCP服务器进程已经终止(虽然本例中已经终止)。 当服务器TCP接受道来自客户的数据时,既然先前打开的哪个套接口的进程已经终止,于是响应一个RST
但是客户进程看不到这个RST,因为它在调用write(即向服务器写数据), 后立即调用readline(即从服务器读数据), 并且由于第二步接收的FIN,所调用的readline立即返回0,于是以出错信息退出
本例的问题: 当FIN到达套接口时,客户正阻塞在fgets调用上。客户实际有两个描述字——-套接口和用户输入,他不能单纯阻塞在这两个源中某个特定的输入上,而是 应该阻塞在其中任何一个源的输入上 。后续select和poll两个函数的目的之一
5.13 SIGPIPE信号 当一个进程向某个已收到RST的套接口执行写操作时,内核向该进程发送一个SIGPIPE信号。该信号缺省行为是终止进程
1 2 3 4 5 6 7 8 9 10 11 12 void str_cli (FILE* fp, int sockfd) { char sendline[MAXLINE], recvline[MAXLIEN]; while (Fgets(sendline, MAXLINE, fp) != NULL ) { Writen(sockfd, sendline, 1 ); sleep(1 ); Writen(sockfd, sendline+1 ,strlen (sendline) -1 ); if (Readlien(sockfd, recvline, MAXLINE) == 0 ) err_quit("str_cli: server terminated prematurely" ); Fputs(recvline, stdout ); } }
sleep(1)把一次数据,分两次写入,当服务器子进程被杀死后,bye的第一次写入,将b写入已经关闭的服务器,收到RST,sleep完后,再次写入,收到SIGPIPE信号
5.14 服务器主机崩溃 查看主机崩溃时会发生什么,先启动服务器,再启动客户,接着键入一行文本以确认连接工作正常,然后从网络上断开服务器主机,并在客户上键入另一行文本。
当服务器主机崩溃时,网络连接发不出任何东西。这里假设是主机崩溃,而不是操作员执行关机。在客户上键入一行文本,由writen写入内核,再由客户TCP作为一个数据分节送出,随后阻塞与readline等待回射应答。 如果用tcpdump观察网络会发现,客户TCP持续重传数据分节,试图从服务器上接收一个ACK。 TCPV2的25.11节,给出TCP重传一个典型模式:源自Berkeley的实现重传数据分节12次,共等待9分钟才放弃重传
为了更快的检测这种情况,可以对readline调用设置一个超时,14.2节将讨论这一点。
我们刚才讨论的情况只有在向服务器发送数据时,才能检测出他已经崩溃。如果想不主动发送数据也能检测服务器主机崩溃,需要采用另一个技术, 7.5讨论的SO_KEEPALIVE套接口选项
5.15 服务器主机崩溃后重启 我们启动服务器和客户,并在客户键入一行文本确认连接已经建立。
服务器主机崩溃并重启
在客户上键入一行文本,它将作为一个TCP数据分节发送到服务器主机
当服务器主机崩溃后重启时,它的TCP丢失了崩溃前的所有连接信息,因此服务器TCP对于所收到的来自客户的数据分节响应一个RST
当客户TCP收到RST时,客户正阻塞与readline调用,导致该调用返回ECONNRESET错误
5.16 服务器主机关机 Unix系统关机时,init进程通常献给所有进程发送SIGTERM信号,再等待固定一段时间,然后给所有仍在运行的进程发送SIGKILL信号,这么做是留给所有运行的进程一小段时间来清除和终止。如果我们不捕获SIGTERM信号并终止,我们的服务器将由SIGKILL信号终止。 所以我们需要使用select或poll函数,使得服务器进程的终止一经发生,客户就马上检测到
5.17 TCP程序例子小结
5.18数据格式 一些常见的问题,如 不同主机字节序不同产生的影响 , 不同实现在存储相同的C数据类型上可能存在差异 , 不同的实现给结构打包的方式,可能由于内存对齐的限制,产生不同 , 常用的解决办法:
所有数值采用文本串来传递,图5.17的做法
显示定义所支持数据类型的二进制格式
5.19 小结
第6章 I/O复用:select 和 poll函数 6.1 概述 这章主要用来解决5.12遇到的问题,客户阻塞在fgets上,而忽略了tcp套接口的输入
如果进程具有一种预先告知内核的1能力,使得内核一旦发现进程指定的一个或多个I/O条件就绪,它就通知进程。这个能力称为I/O复用,是由select和poll两个函数支持的
I/O复用典型使用在下列网络应用场合:
当客户处理多个描述字(如5.12的交互式输入和网络套接口),必须使用I/O复用
一个客户同时处理多个套接口是可能的,不过比较少见。见16.5节
如果一个TCP服务器既要处理监听套接口,又要处理已经连接的套接口,参考后面的示例
如果一个服务器既要处理TCP,又要处理UDP,一般要使用I/O复用
如果一个服务器需要处理多个服务或多个协议
6.2 I/O模型 Unix可用的I/O模型有5种:
阻塞I/O
非阻塞I/O
I/O复用(select 和 poll)
信号驱动I/O(SIGIO)
异步I/O(POSIX的aio_系列函数)
一个输入操作通常包括两个不同的阶段
等待数据准备好
从内核到进程拷贝数据
阻塞I/O模型
本书截至目前为止,所有的例子都为阻塞I/O
非阻塞I/O模型
当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误,此处为EWOULDBLOCK
I/O复用模型
阻塞与select或poll这两个系统调用的某一个之上,而不是阻塞在真正的I/O系统调用上
目前看来I/O复用没有什么优势,还多了一个系统调用,优势参考后面的详解
信号驱动I/O模型
优势在于等待数据包到达期间,进程不被阻塞。主循环可用继续执行,只要不时等待来自信号处理函数的通知:既可以是已数据已被准备好被处理,也可以是数据报已准备好被读取
异步I/O模型
异步I/O由POSIX规范定义。后来演变成当前POSIX规范的各种早期标准定义的实时函数中存在的差异已经取得一致。这些函数的工作机制是:告诉内核启动某个操作,并让内核在整个操作完成后通知我们。
和信号驱动I/O的区别在于:后者是内核通知我们何时可用启动一个I/O操作,而前者是通知我们何时完成
各种I/O模型的比较
同步I/O和异步I/O
同步:导致请求进程阻塞,直到I/O操作完成
异步:不导致请求进程阻塞
上述的前四种为同步I/O,只有异步I/O模型和与POSIX定义的异步I/O相匹配
6.3 select函数 1 2 3 4 5 6 7 8 9 10 #include <sys/select.h> #include <sys/time.h> int select (int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout) ; struct timeval { long tv_sec; long tv_usec; };
timeout有三种可能:
设置为空指针,永远等下去
等待一定固定时间,不超过该参数设置的值
不等待,检查描述字后立即返回,轮询 。 参数设置为0
中间的三个参数分别为:我们要让内核测试的读,写和异常条件的描述字。目前支持异常条件只有两个:
某个套接口的带外数据的到达。24章
某个已设为分组方式的伪终端存在可从其主端读取的控制状态信息。
1 2 3 4 5 6 7 8 9 10 11 12 void FD_ZERO (fd_set* fdset) ; void FD_SET (int fd, fd_set* fdset) ; void FD_CLR (int fd, fd_set* fdset) ; int FD_ISSET (int fd, fd_set* fdset) ; fd_set rset; FD_ZERO(&rset); FD_SET(1 , &rest); FD_SET(4 , &rset); FD_SET(5 , &rest);
6.4 str_cli函数
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 #include "unp.h" void str_cli (FILE *fp, int sockfd) { int maxfdp1; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset); for ( ; ; ) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1 ; Select(maxfdp1, &rset, NULL , NULL , NULL ); if (FD_ISSET(sockfd, &rset)) { if (Readline(sockfd, recvline, MAXLINE) == 0 ) err_quit("str_cli: server terminated prematurely" ); Fputs(recvline, stdout ); } if (FD_ISSET(fileno(fp), &rset)) { if (Fgets(sendline, MAXLINE, fp) == NULL ) return ; Writen(sockfd, sendline, strlen (sendline)); } } } void str_cli2 (FILE* fp, int sockfd) { int maxfdp1, n; fd_set rset; char sendline[MAXLINE], recvline[MAXLINE]; FD_ZERO(&rset); for (;;) { FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1 ; select(maxfdp1, &rset, NULL , NULL , NULL ); if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, recvline, MAXLINE)) == -1 ) perror("read error" ); write(STDOUT_FILENO, recvline, n); } if (FD_ISSET(fileno(fp), &rset)) { if (fgets(sendline, MAXLINE, fp) == NULL ) return ; write(sockfd, sendline, strlen (sendline)); } } }
注意maxfdp1参数,要比实际最大描述字大1
6.6 批量输入
6.5改进后仍存在问题,假设输入文件只有9行。最后一行在时刻8发出,如上图(6.11)所示。写完这个请求后,我们不能立即关闭连接,因为管道中还有其他的请求和应答。问题的引起在于我们对标准输入中的EOF的处理:str_cli函数就此返回到main函数,而main函数随后终止。然而在批量方式下,标准输入中的EOF并不意味着我们同时也完成了从套接口的读入;可能仍有请求在去往服务器的路上,或者仍有应答在返回客户的路上。所以我们需要一种半关闭的方式,这也是下章介绍的
6.6 shutdown函数 close的两个限制:
close把引用计数减1,仅在该计数变为0的时候才关闭接口。
close终止数据传送的两个方向:读和写。既然TCP连接是双全工的,有时候我们只需要关闭一般
1 2 3 4 5 6 7 8 #include <sys/socket.h> int shutdown (int sockfd, int howto) ; howto的参数值: SHUT_RD:关闭连接读的这一半 SHUT_WR:关闭连接写的这一半 -- 对于TCP套接口,这称为半关闭(half-close) SHUT_RDWR:读写都关闭 -- 这与调用两次shutdown分别SHUT_RD 和 SHUT_WR等效
6.7 str_cli函数(再修订版) 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 void str_cli3 (FILE* fp, int sockfd) { int maxfdp1, stdineof; fd_set rset; char buf[MAXLINE]; int n; stdineof = 0 ; FD_ZERO(&rset); for (;;) { if (stdineof == 0 ) FD_SET(fileno(fp), &rset); FD_SET(sockfd, &rset); maxfdp1 = max(fileno(fp), sockfd) + 1 ; select(maxfdp1, &rset, NULL , NULL , NULL ); if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, buf, MAXLINE)) == 0 ) { if (stdineof == 1 ) return ; else perror("str_cli: server terminated prematurely" ); } write(STDOUT_FILENO, buf, n); } if (FD_ISSET(fileno(fp), &rset)) { if ((n = read(fileno(fp), buf, MAXLINE)) == 0 ) { stdineof = 1 ; shutdown(sockfd, SHUT_WR); FD_CLR(fileno(fp), &rset); continue ; } write(sockfd, buf, n); } } }
用stdineof标记是否stdin端读到EOF,若读到,则将stdin描述字从rset中清除,等sockfd读完后,返回main函数
6.8 TCP回射服务器程序(修订版) 用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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <unistd.h> #include <sys/socket.h> #include <sys/select.h> #include <sys/time.h> #include <netinet/in.h> #include <stdio.h> #include <errno.h> #include <stdlib.h> #include <strings.h> #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 #define MAXLINE 1024 int main (int argc, char ** argv) { int i, maxi, maxfd, listenfd, connfd, sockfd; int nready, client[FD_SETSIZE]; ssize_t n; fd_set rset, allset; char buf[MAXLINE]; socklen_t clilen; struct sockaddr_in cliaddr , servaddr ; if ((listenfd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) perror("socket error" ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) perror("bind error" ); if (listen(listenfd, 20 ) < 0 ) perror("listen error" ); printf ("ready to accept\n" ); maxfd = listenfd; maxi = -1 ; for (i = 0 ; i < FD_SETSIZE; i++) client[i] = -1 ; FD_ZERO(&allset); FD_SET(listenfd, &allset); for (;;) { rset = allset; nready = select(maxfd+1 , &rset, NULL , NULL , NULL ); if (FD_ISSET(listenfd, &rset)) { clilen = sizeof (cliaddr); if ((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0 ) perror("accept error" ); printf ("connect success form,client IP:**, client port:%d\n" , ntohs(cliaddr.sin_port)); for (i = 0 ; i < FD_SETSIZE; i++) if (client[i] < 0 ) { client[i] = connfd; break ; } if (i == FD_SETSIZE) { printf ("too many clients\n" ); exit (0 ); } FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; if (i > maxi) maxi = i; if (--nready <= 0 ) continue ; } for (i = 0 ; i <=maxi; i++) { if ((sockfd = client[i]) < 0 ) continue ; if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, buf, MAXLINE)) == 0 ) { close(sockfd); FD_CLR(sockfd, &allset); client[i] = -1 ; } else write(sockfd, buf, n); if (--nready <= 0 ) break ; } } } return 0 ; }
仍存在问题,如果某一客户端,发送一个字节的数据(不是换行符)后进入睡眠。服务器读入这一个字节的数据后,会阻塞与下一个read调用,以等待来自客户的其余数据。这样,服务器就被单个用户阻塞了。
解决方法:
使用非阻塞I/O
让每个客户由单独的控制线程提供服务
对I/O操作设置一个超时
6.9 pselect函数 1 2 3 4 5 6 7 8 9 10 11 #include <sys/select.h> #include <signal.h> #include <time.h> int pselect (int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timesepc* timeout, const sigset_t * sigmask) ; struct timespec { time_t tv_sec; long tv_nsec; }
与select的相比两个变化:
使用timespec结构,精度由微妙到纳秒了
增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再由测试这些当前被禁止的信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码
简单来说,在pselect期间,以一新的信号屏蔽集替换当前的,pselect返回之后替换回来
6.10 poll函数 poll函数提供的功能与select类似,但在处理流设备时,它能提供额外的信息
1 2 3 4 5 6 7 8 9 10 11 #include <poll.h> int poll (struct pollfd* fdarray, unsigned long nfds, int timeout) ; struct pollfd { int fd; short events; short revents; }
结构数组中元素的个数由nfds参数指定。timeout参数指定poll函数返回前等待多长时间。
如果不关心某个特定描述字,可以把它对应的pollf的fd成员设置成一个负值
6.11 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 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 <unistd.h> #include <sys/socket.h> #include <netinet/in.h> #include <strings.h> #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <poll.h> #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 #define MAXLINE 1024 #define INFTIM -1 #define OPEN_MAX 1024 int main (int argc, char ** argv) { int i, maxi, listenfd, connfd, sockfd; int nready; ssize_t n; char buf[MAXLINE]; socklen_t clilen; struct pollfd client [OPEN_MAX ]; struct sockaddr_in cliaddr , servaddr ; if ((listenfd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) perror("socket error" ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); if (bind(listenfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) < 0 ) perror("bind error" ); if (listen(listenfd,20 ) < 0 ) perror("listen error" ); printf ("ready to accept\n" ); client[0 ].fd = listenfd; client[0 ].events = POLLRDNORM; for (i = 1 ; i < OPEN_MAX; i++) client[i].fd = -1 ; maxi = 0 ; for (;;) { nready = poll(client, maxi+1 , INFTIM); if (client[0 ].revents & POLLRDNORM) { clilen = sizeof (cliaddr); if ((connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &clilen)) < 0 ) perror("accept error" ); printf ("connect success form,client IP:**, client port:%d\n" , ntohs(cliaddr.sin_port)); for (i = 1 ; i < OPEN_MAX; i++) if (client[i].fd < 0 ) { client[i].fd = connfd; break ; } if (i == OPEN_MAX) { printf ("too many client\n" ); exit (0 ); } client[i].events = POLLRDNORM; if (i > maxi) maxi = i; if (--nready <=0 ) continue ; } for (i = 1 ; i <= maxi; i++) { if ((sockfd = client[i].fd) < 0 ) continue ; if (client[i].revents & (POLLRDNORM | POLLERR)) { if ((n = read(sockfd, buf, MAXLINE)) < 0 ) { if (errno == ECONNRESET) { close(sockfd); client[i].fd = -1 ; } else perror("read error" ); } else if (n == 0 ) { close(sockfd); client[i].fd = -1 ; } else write(sockfd, buf, n); if (--nready <= 0 ) break ; } } } }
6.12 小结 Unix提供了5种不同的I/O模型:
阻塞I/O模型
非阻塞I/O模型
I/O复用模型
信号驱动I/O模型
异步I/O模型
第7章 套接口选项 7.1 概述 第8章 基本UDP套接口编程 8.1 概述 UDP是无连接不可靠的数据报协议,不同于TCP提供的面向连接的可靠字节流
8.2 recvfrom和sendto函数 1 2 3 4 5 6 #include <sys/socket.h> ssize_t recvfrom (int sockfd, void * buf, size_t nbytes, int flags, struct sockaddr* from, socklen_t * addrlen) ;ssize_t sendto (int sockfd, const void * buff, size_t nbytes, int flags, const sturct sockaddr* to, socklen_t addrlen) ;
**flags:**在14章讨论,目前总把flags设为0
**to:**指向一个含有数据报接收者的协议地址的套接口地址结构,其大小由addrlen参数指定
**from:**类似于accept的后两个参数,告诉我们是谁发送了数据报(udp情况下)或是谁发起了连接(tcp情况下)
写一个长度为0的数据报是可行的。在UDP情况下,这导致一个只包含一个IP头部(IPV4 20字节,IPV6 40字节)和一个8字节的UDP头部而没有数据的IP数据报。
8.3 UDP回射服务器程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include "unp.h" int main (int argc, char ** argv) { int sockfd; struct sockaddr_in servaddr , cliaddr ; sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&servadddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); dg_echo(sockfd, (struct sockaddr*)&cliaddr, sizeof (cliaddr)); }
8.4 UDP回射服务器程序:dg_echo函数 1 2 3 4 5 6 7 8 9 10 11 void dg_echo (int sockfd, struct sockaddr* pcliaddr, socklen_t clilen) { int n; socklen_t len; char mesg[MAXLINE]; for (;;) { len = clilen; n = recvfrom(sockfd, mesg, MAXLINE, 0 , pcliaddr, &len); sendto(sockfd, mesg, n, 0 , pcliaddr, len); } }
一般来说TCP服务器是并发的,而大多数UDP服务器是迭代的
UDP为了防止丢包,可以通过改变缓冲区大小,通过使用setsockpot,见第七章
8.5 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 #include <unistd.h> #include <strings.h> #include <sys/socket.h> #include <netinet/in.h> #include <errno.h> #include <stdio.h> #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 #define MAXLINE 1024 int main (int argc, char ** argv) { int sockfd; struct sockaddr_in servaddr ; bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, SERV_IP, &servaddr.sin_addr.s_addr); sockfd = socket(AF_INET, SOCK_DGRAM, 0 ); dg_cli(stdin , sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); return 0 ; }
8.6 UDP回射客户程序:dg_cli函数 1 2 3 4 5 6 7 8 9 10 11 12 13 void dg_cli(FILE* fp, int sockfd, const struct sockaddr* pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE+1]; while (fgets(sendline, MAXLINE, fp) != NULL) { sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); recvline[n] = 0; fputs(recvline, stdout); } }
8.7 数据包的丢失 如果一个客户数据报丢失(譬如被客户主机与服务器主机之间的某个服务器丢弃),客户讲永远阻塞于recvfrom调用
8.8 验证收到的响应 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void dg_cli (FILE* fp, int sockfd, const struct sockaddr* pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE+1 ]; socklen_t len; struct sockaddr * preply_addr ; preply_addr = (struct sockaddr*)malloc (servlen); while (fgets(sendline, MAXLINE, fp) != NULL ) { sendto(sockfd, sendline, strlen (sendline), 0 , pservaddr, servlen); len = servlen; n = recvfrom(sockfd, recvline, MAXLINE, 0 , preply_addr, &len); if (len != servlen || memcmp (pservaddr, prepl_addr, len) != 0 ) { printf ("reply from %s (ignored)\n" ), Sock_ntop(preply_addr, len)); continue ; } recvline[n] = 0 ; fputs (recvline, stdout ); } }
当服务器运行在具有多个接口和ip地址的主机上时,可能失败
8.9 服务器进程未运行 运行客户端,但不启动服务器
tcpdump的输出
可以看到服务器响应一个”port unreadchable”ICMP消息,但这个错误不返回给客户进程
该ICMP错误为异步错误,由sendto引起,但sendto本身却成功返回。UDP输出操作成功返回,仅仅表示在接口输出队列中具有存放所导致IP数据报的空间。该ICMP错误直到后来才返回,所以称之为异步.
一个基本规则:对于一个UDP套接口,由它引发的异步错误不返回给它,除非它已连接。
8.10 UDP程序例子小结
8.11 UDP的connect函数
未连接UDP套接口,新创建UDP套接口缺省如此
已连接UDP套接口,对UDP套接口调用connect的结果
对于已连接套接口,与缺省的未连接UDP套接口相比,发生了三个变化:
我们不能给输出操作指定宿IP地址和端口号。也就是说,我们不使用sendto,而改用write或send。写到已连接UDP套接口上的任何内容都自动发送到由connect指定的协议地址
我们不必使用recvfrom以获得数据报的发送者,而改用read,recv或recvmsg。在一个已连接UDP套接口上由内核为输入操作返回的数据报仅仅是那些来自connect所指定协议地址的数据报。这样就限制一个已连接UDP套接口能且仅能与一个对端交换数据报
由已连接UDP套接口引发的异步错误将返回给它们所在的进程。
给一个UDP套接口多次调用connect
两个目的:
指定新的IP地址和端口号
断开套接口
不同于TCP套接口中的connect的使用:对于TCP套接口,connect只能调用一次
为断开一个已连接的UDP套接口,再次调用connect时,把套接口地址结构成员设置为AF_UNSPEC
性能
对于一个未连接的UDP套接口调用sendto的步骤如下:
连接套接口
输出第一个数据报
断开套接口连接
连接套接口
输出第二个数据报
断开套接口
对于已连接的
连接套接口
输出第一个
输出第二个
8.12 dg_cli函数(修订) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void dg_cli2 (FILE* fp, int sockfd, const struct sockaddr* pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE+1 ]; connect(sockfd, (struct sockaddr*)&pservaddr, servlen); while (fgets(sendline, MAXLINE, fp) != NULL ) { write(sockfd, sendline, strlen (sendline)); n = read(sockfd, recvline, MAXLINE); recvline[n] = 0 ; fputs (recvline, stdout ); } }
8.13 UDP缺乏流量控制 本小节执行了一个测试函数,发送2000个1400字节大小的UDP数据报给服务器
发出2000个数据报,但服务器只收到其中的30个,丢失率未98%。对于服务器或客户端都没有给出任何指示说这些数据报已丢失。证实了我们说过的话,即UDP没有流量控制,是不可靠。本例表面UDP发送端淹没其接受端是轻而易举的事情。
由UDP给某个特定套接口排队的UDP数据报数目受限于该套接口接收缓冲区的大小。我们可以使用SO_RCVBUF套接口修改该值,见前一章,以及上述视频笔记部分 UDP
8.14 UDP中的外出接口的确定 以连接UDP套接口还可以用来确定用于某个特定目的地的外出接口。这是由connect函数应用到UDP套接口时的一个副作用造成的:内核选择本地IP地址(假设其进程未曾调用bind显示指派他)。这个本地IP地址搜索路由表得到外出接口,然后选用该接口的主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 #include "unp.h" int main (int argc, char ** argv) { int sockfd; socklen_t len; struct sockaddr_in cliaddr , servaddr ; if (argc != 2 ) err_quit("usage: udpcli <IPaddrss>" ); sockfd = socket(AF_INET, SOCK_DGREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1 ], &servaddr.sin_addr.s_addr); connect(sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); len = sizeof (cliaddr); getsockname(sockfd, (struct sokcaddr*)&cliaddr, &len); printf ("local addrss %s\n" , Sock_ntop((struct sockaddr*)&cliaddr, len)); return 0 ; }
8.15 使用select函数的TCP和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 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 #include <unistd.h> #include <sys/socket.h> #include <strings.h> #include <sys/select.h> #include <arpa/inet.h> #include <stdio.h> #include <sys/types.h> #include <netinet/in.h> #include <signal.h> #include <errno.h> #define MAXLINE 1024 #define SERV_IP "10.0.0.14" #define SERV_PORT 7777 int max (int a, int b) { return a > b ? a : b; } int main (int argc, char ** argv) { int listenfd, connfd, updfd, nready, maxfdp1; char mesg[MAXLINE]; pid_t childpid; fd_set rset; ssize_t n; socklen_t len; const int on = 1 ; struct sockaddr_in cliaddr , servaddr ; void sig_chld (int ) ; listenfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)); bind(listenfd, (sturct sockaddr*)&servaddr, sizeof (servaddr)); listen(listenfd, 20 ); updfd = socket(AF_INET, SOCK_DGRAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); servaddr.sin_addr.s_addr = htonl(INADDR_ANY); bind(updfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); signal(SIGCHLD, sig_chld); FD_ZERO(&rset); maxfdp1 = max(listenfd, updfd) + 1 ; for (;;) { if ((nready = select(maxfdp1, &rset, NULL , NULL , NULL )) < 0 ) { if (errno == EINTR) continue ; else perror("select error" ); } if (FD_ISSET(listenfd, &rset)) { len = sizeof (cliaddr); connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len); if ((childpid = fork()) == 0 ) { close(listenfd); str_echo(connfd); exit (0 ); } close(connfd); } if (FD_ISSET(updfd, &rset)) { len = sizeof (cliaddr); n = recvfrom(udpfd, mesg, MAXLINE, 0 , (struct sockaddr*)&cliaddr, &len); sendto(updfd, mesg, n, 0 , (struct sockaddr*)&cliaddr, len); } } }
8.16 小结 把TCP客户/服务器更换成UDP很容易,但失去了许多功能:检测丢失的分组并重传,验证响应是否来自正确的对端等等
UDP套接口可能产生异步错误,它们是在其引发分组发送完一段时间之后才报告错误。TCP套接口总是给应用进程报告这些错误,但是UDP套接口必须已连接才能接收这些错误。
UDP没有流量控制,这一点很容易演示。但由于UDP的应用程序构造模式一般为请求-应答,不传送大数据,所以一般不成问题
第11章 名字与地址转换 11.1 概述 到目前为止,本书所有的例子都用数值地址来表示主机(如206.6.226.33),用数值端口号来标识服务器(如端口13代表标准的daytime服务器)。我们处于许多理由,我们应该使用名字而不是数值。本章讲述在名字和数值地址间进行转换的函数:gethostbyname和gethostbyaddr在主机名字与IPV4地址之间进行转换;getservbyname和getservbyport在服务名字和端口之间进行转换。以及两个协议无关的转换函数:getaddrinfo和getnameinfo
11.2 域名系统 域名系统(Domain Name System),简称DNS
用于主机名字与IP地址之间的映射。
主机名既可以是一个简单名字,例如solaris或freebsd,也可以是一个全限定域名(Fully Qualified Domain Name,简称FQDN),例如solaris.unpbook.com
资源记录
DNS中的条目称为资源记录(resource record,简称RR),我们感兴趣的RR类型只有若干个
解析器和名字服务器
每个组织机构往往运行一个或多个名字服务器(name server),它们通常就是所谓的BIND(Berkeley Internet Name Domain的简称)程序。诸如偶们在本书中编写的客户和服务器等应用程序通过调用称为 解析器(resolver) 的函数库接触DNS服务器。常见的解析器函数是将在本章讲都gethostbyname和gethostbyaddr
解析器代码通常包含在一个系统函数库中,在构造应用程序时被 链编 到应用程序中。另有些系统提供一个由全体应用程序共享的集中式解析器守护进程,并提供相这个守护进程执行RPC的系统函数库代码。
解析器代码通常读取其系统相关配置文件确定本组织机构的名字服务器们的所在位置。文件/etc/resolv.conf通常包含本地名字服务器主机的IP地址
解析器使用UDP向本地名字服务器发出查询,如果本地名字服务器不知道答案,它通常就会使用UDP在整个因特网上查询其他名字服务器。如果名字太长,超出UDP消息的承载能力,本地名字服务器和解析器会切换到TCP
11.3 gethostbyname函数 查找主机名最基本的函数是gethostbyname。如果调用成功,它就返回一个指向hostent结构的指针,该结构中含有所查找主机的所有IPV4地址。这个函数的局限是只能返回IPV4地址。
1 2 3 4 5 6 7 8 9 10 11 12 #include <netdb.h> struct hostent* gethostbyname (const char * hostname) ; struct hostent { char * h_name; char ** h_aliases; int h_addrtype; int h_length; char ** h_addr_list; }
按照DNS的说法,getbyname执行的是对A记录的查询。他只能返回IPV4地址。
下图所查询的主机名有2个别名和3个IPV4地址。
gethostbyname发生错误时,它不设置errno变量,而是将全局整数变量h_errno设置为在<netdb.h>中定义的下列常值之一:
HOST_NOT_FOUND
TRY_AGAIN
NO_RECOVERY
NO_DATA(等同于NO_ADDRESS)
多数解析器提供名为hstrerror的函数,以某个h_errno值作为唯一的参数
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 #include <unistd.h> #include <netdb.h> #include <sys/socket.h> #include <errno.h> #include <arpa/inet.h> #include <stdlib.h> #include <stdio.h> int main (int argc, char ** argv) { char *ptr, **pptr; char str[INET_ADDRSTRLEN]; struct hostent *hptr ; while (--argc > 0 ) { ptr = *++argv; if ((hptr = gethostbyname(ptr)) == NULL ) { printf ("gethostbyname error for host%s:%s" , ptr, hstrerror(h_errno)); continue ; } printf ("official hostname%s\n" , hptr->h_name); for (pptr = hptr->h_aliases; *pptr != NULL ; pptr++) printf ("\talias: %s\n" , *pptr); switch (hptr->h_addrtype) { case AF_INET: pptr = hptr->h_addr_list; for (; *pptr != NULL ; pptr++) printf ("\taddress: %s\n" , inet_ntop(hptr->h_addrtype, *pptr, str, sizeof (str))); break ; default : perror("unkonwn address type" ); break ; } } exit (0 ); } ubuntu@VM-0 -14 -ubuntu:~/learning/2023 /UnpStudy/chapter11$ hostnamectl Static hostname: VM-0 -14 -ubuntu Icon name: computer-vm Chassis: vm Machine ID: 075097 c3da50476181f422ef916e1460 Boot ID: 149585e4 f7384a71833fa6a9f2b6293d Virtualization: kvm Operating System: Ubuntu 20.04 LTS Kernel: Linux 5.4 .0 -126 -generic Architecture: x86-64 ubuntu@VM-0 -14 -ubuntu:~/learning/2023 /UnpStudy/chapter11$ ./hostent VM-0 -14 -ubuntu official hostnamelocalhost.localdomain alias: VM-0 -14 -ubuntu address: 127.0 .1 .1
正式主机名就是FQDN
11.4 gethostbyaddr函数 gethostbyaddr函数试图由一个二进制的IP地址找到响应的主机名,与gethostbyname的行为刚好相反
1 2 3 #include <netdb.h> struct hostent* gethostbyaddr (const char * addr, socklen_t len, int family) ;
adrr参数实际上不是char* 类型, 而是一个指向存放ipv4地址的某个in_addr结构的指针,len参数是这个结构的大小,对于IPV4地址为4,family参数为AF_INET
按照DNS的说法,gethostbyaddr在in_addr.arpa域中向一个名字服务器查询PTR记录
11.5 getservbyname和getservbyport函数 如果我们在程序代码中通过其名字而不是其端口号来指代一个服务,而且从名字到端口号的映射关系保存在一个文件中(通常是/etc/services)。getservbyname函数用于根据给定名字查找相应服务
1 2 3 4 5 6 7 8 9 10 11 #include <netdb.h> struct servent* getservbyname (const char * servname, const char * protoname) ; struct servent { char *s_name; char **s_aliases; int s_port; char *s_proto; };
服务器参数servname必须指定。如果同时指定了协议(protoname参数为非空指针),那么指定服务必须有匹配的协议。
1 2 3 4 5 6 struct servent *sptr ;sptr = getservbyname("domain" , "udp" ); sptr = getservbyname("ftp" , "tcp" ); sptr = getservbyname("ftp" , NULL ); sptr = getservbyname("ftp" , "udp" );
既然FTP仅支持TCP,第2个调用和第3个调用等效,第4个调用则会失败。以下是/etc/services文件中的典型文本行:
getservbyport用于根据端口号和可选协议查找相应服务
1 2 3 #include <netdb.h> struct servent* getservbyport (int port, const char * protoname) ;
port的参数值必须为网络字节序,典型调用如下:
1 2 3 4 5 struct servent *sptr ;sptr = getservbyport(htons(53 ), "udp" ); sptr = getservbyport(htons(21 ), "tcp" ); sptr = getservbyport(htons(21 ), NULL ); sptr = getservbyport(htons(21 ), "udp" );
既然UDP上没有服务使用端口21, 最后一个调用将失败
有些端口号在TCP上用于一种服务,在UDP上却用于完全不同的另一种服务,例如
例子:使用gethostbyname和getservbyname
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 #include "unp.h" int main (int argc, char **argv) { int sockfd, n; char recvline[MAXLINE+1 ]; struct sockaddr_in servaddr ; struct in_addr **pptr ; struct in_addr *inetaddrp [2]; struct in_addr inetaddr ; struct hostent *hp ; struct servent *sp ; if (argc != 3 ) err_quit("usage: daytimetcpcli1 <hostname> <service> " ); if ((hp = gethostbyname(argv[1 ])) == NULL ) { if (inet_aton(argv[1 ], &inetaddr) == 0 ) { err_quit("hostname error for %s: %s" , argv[1 ], hstrerror(h_error)); } else { inetaddrp[0 ] = &inetaddr; inetaddrp[1 ] = NULL ; pptr = inetaddrp; } } else { pptr = (struct in_addr **)hp->h_addr_list; } if ((sp = getservbyname(argv[2 ], "tcp" )) == NULL ) err_quit("getservbyname error for %s" , argv[2 ]); for (; *pptr != NULL ; pptr++) { sockfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = sp->s_port; memcpy (&servaddr.sin_addr, *pptr, sizeof (struct in_addr)); printf ("trying %s\n" , Sock_ntop((struct sockaddr*)&servaddr, sizeof (servaddr))); if (connect(sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)) == 0 ) break ; err_ret("connect error" ); close(sockfd); } if (*pptr == NULL ) err_quit("unable to connect" ); while ((n = read(sockfd, recvline, MAXLINE)) > 0 ) { recvline[n] = 0 ; fputs (recvline, stdout ); } exit (0 ); }
输出
11.6 getaddrinfo 函数 gehostbyname 和gethostbyaddr 这两个函数仅仅支持IPV4。解析IPV6地址的API经历了若干次反复:最终结果是getaddrinfo 函数。
getaadrinfo 函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个sockaddr结构的链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <netdb.h> int getaadrinfo (const char * hostname, const char * service, const struct addrinfo * hints, struct addrinfo **result) ; struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; socklen_t ai_addrlen; char *ai_canonname; struct sockaddr *ai_addr ; struct addrinfo *ai_next ; };
前两个参数没啥好说的,第3个hints参数,调用者在这个结构中填入关于期望返回的信息类型的暗示。比如希望指定的服务返回UDP的套接口,则可以把hints结构中的ai_socktype成员设置为SOCK_DGRAM
hints结构中调用者可以设置的成员有:
ai_flags(零个或多个或在一起的AI_xxx值)
ai_family(某个AF_xxx值)
ai_socktype(某个SOCK_xxx值)
ai_protocol
如果函数成功返回0,那么由result参数指向的变量被填入一个指针,指向该结构链表的指针。可导致返回多个addrinfo结构的情形有以下两个:
hostname有多个关联地址的话,每个满足hints参数的地址都返回一个对应的结构
service指定的服务支持多个套接口类型的话,同上,每个套接口返回一个对应结构
举例来说,在没有提供hints参数的情况下,请求查找2个IP地址的某个主机上的domain服务(DNS服务既支持TCP也支持UDP),那么将返回4个addrinfo结构:
第1个IP地址+SOCK_STREAM
第1个IP地址+SOCK_DGREAM
第2个IP地址+SOCK_STREAM
第2个IP地址+SOCK_DGRAM
addrinfo结构中返回的信息,可以直接用于socket调用
如果结构中设置了 AI_CANONNAME 标志,那么本函数返回的第一个addrinfo结构的ai_canonname成员指向所查找主机的规范名字。
1 2 3 4 5 6 struct addrinfo hints , *res ;bzero(&hints, sizeof (hints)); hints.ai_flags = AI_CANONNAME; hints.ai_family = AF_INET; getaadrinfo("freebsd4" , "domain" , &hints, &res);
getaddrinfo解决了把主机名和服务名转换成套接口地址结构的问题。11.17节讲解它的反义函数getnameinfo,它把套接口地址结构转换成主机名和服务名
11.7 gai_strerror 函数 和之前的strerror和hstrerror差不多
1 2 3 #include <netdb.h> const char * gai_strerror (int error) ;
11.8 freeaddrinfo 函数 用来清除getaddrinfo返回的addrinfo链表,这个链表的所有结构以及由它们指向的任何动态存储空间(譬如套接口地址结构和规范主机名)都被释放掉
1 2 #include <netdb.h> void freeaddrinfo (struct addrinfo * ai) ;
11.9 getaddrinfo函数: IPV6 POSIX规范定义了getaddrinfo函数以及该函数为IPV4或IPV6返回的信息。在以图11.8汇总这些返回值之前,我们注意以下几点:
11.10 getaadrinfo 函数:例子 在附加hitns期望时的,返回结果
11.11 host_serv 函数 一个getaddrinfo的接口函数,增加我们刚兴趣的两个成员,family和socktype作为参数,其他实现在函数内部进行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include "unp.h" struct addrinfo * host_serv (const char * hostname, const char * service, int family, int socktype) ; struct addrinfo * host_serv (const char * hostname, const char * service, int family, int socktype) { int n; struct addrinfo hints , *res ; bzero(&hints, sizeof (hints)); hints.ai_flags = AI_CANONNAME; hints.ai_family = family; hints.ai_socktype = socktype; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0 ) return NULL ; return res; }
11.12 tcp_connect 函数 也是一个自定义函数。处理TCP客户和服务器大多数情形的两个函数。第一个即tcp_connect执行客户的通常步骤:创建一个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 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include "unp.h" int tcp_connect (const char * hostname, const char * service) ; int tcp_connect (const char * hostname, const char * service) { int sockfd, n; struct addrinfo hints , *res , *ressave ; bzero(&hints, sizeof (struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0 ) err_quit("tcp_connect error for %s, %s: %s" , host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0 ) continue ; if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0 ) break ; close(sockfd); } while ((res = res->ai_next) != NULL ); if (res == NULL ) err_sys("tcp_connect error for %s, %s" , host, serv); freeaddrinfo(ressave); return (sockfd); } int Tcp_connect (const char * host, const char * serv) { return (tcp_connect(host, serv)); } #include "unp.h" int main (int argc, char **argv) { int sockfd, n; char recvline[MAXLINE+1 ]; socklen_t len; struct sockaddr_storage ss ; if (argc != 3 ) err_quit("usage: daytimetcpcli <hostname/IPaddress> <service/port# >" ); sockfd = Tcp_connect(argv[1 ], argv[2 ]); len = sizeof (ss); Getpeername(sockfd, (struct sockaddr*)&ss, &len); printf ("connected to %s\n" , Sock_ntop_host((struct sockaddr*)&ss, len)); while ((n = read(sockfd, recvline, MAXLINE)) > 0 ) { recvline[n] = 0 ; fputs (recvline, stdout ); } exit (0 ); }
11.13 tcp_listen 函数 创建一个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 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 #include <unistd.h> #include <sys/socket.h> #include <strings.h> #include <sys/types.h> #include <netdb.h> #include <netinet/in.h> #include <stdlib.h> #include <errno.h> #include <time.h> #include <arpa/inet.h> #include <string.h> #include <stdio.h> #define MAXLINE 1024 #define SERV_PORT 7777 #define SERV_IP "10.0.0.14" int tcp_listen (const char * host, const char * serv, socklen_t * addrlenp) ;int main (int argc, char **argv) { int listenfd, connfd; socklen_t len; char buf[MAXLINE]; time_t ticks; struct sockaddr_storage cliaddr ; if (argc != 2 ) { printf ("usage: daytimetcpservl <service or port#>\n" ); exit (0 ); } listenfd = tcp_listen(NULL , argv[1 ], NULL ); for (;;) { len = sizeof (cliaddr); connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len); printf ("connection success\n" ); ticks = time(NULL ); snprintf (buf, sizeof (buf), "% .24s\r\n" , ctime(&ticks)); write(connfd, buf, strlen (buf)); close(connfd); } } int tcp_listen (const char * host, const char * serv, socklen_t * addrlenp) { int listenfd, n; const int on = 1 ; struct addrinfo hints , *res , *ressave ; bzero(&hints, sizeof (struct addrinfo)); hints.ai_flags = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; if ((n = getaddrinfo(host, serv, &hints, &res)) != 0 ) { gai_strerror(n); exit (0 ); } ressave = res; do { listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (listenfd < 0 ) continue ; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof (on)); if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0 ) break ; close(listenfd); } while ((res = res->ai_next) != NULL ); if (res == NULL ) { printf ("tcp_listen error\n" ); exit (0 ); } printf ("information of aadrinfo:\n" ); if (res->ai_family == AF_INET); printf ("ai_family = AI_INET\n" ); if (res->ai_socktype == SOCK_STREAM) printf ("ai_socktype = SOCK_STREAM\n" ); struct sockaddr_in sin ; memcpy (&sin , res->ai_addr, sizeof (sin )); printf ("ai_addr = %s\n" , inet_ntoa(sin .sin_addr)); listen(listenfd, 20 ); if (addrlenp) *addrlenp = res->ai_addrlen; freeaddrinfo(ressave); return (listenfd); }
11.4 udp_client 函数 本节创建一个未连接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 53 54 55 56 57 58 59 60 61 62 63 #include "unp.h" int udp_client (const char * hostname, const char * service, struct sockaddr **saptr, socklen_t *lenp) ;int udp_client (const char * hostname, const char * service, struct sockaddr **saptr, socklen_t *lenp) { int sockfd, n; struct addrinfo hints , *res , *ressave ; bzero(&hints, sizeof (struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGREAM; if ((n = getaadrinfo(host, serv, &hints, &res)) != 0 ) err_quit("udp_client error for %s, %s: %s" , host, serv, gai_strerror(n)); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd >= 0 ) break ; } while ((res = res->ai_next) != NULL ); if (res == NULL ) err_sys("udp_client error" ); *saptr = (struct sockaddr*)malloc (res->ai_addrlen); memcpy (*saptr, res->ai_addr, res->ai_addrlen); *lenp = res->ai_addrlen; freeaddrinfo(ressave); retrun (sockfd); } #include "unp.h" int main (int argc, char **argv) { int sockfd, n; char recvline[MAXLINE+1 ]; socklen_t salen; struct sockaddr *sa ; if (argc != 3 ) err_quit("usage:daytimeupdcil <hostname/IPaddress> <service/port#>" ); sockfd = udp_client(argv[1 ], argv[2 ], &sa, &salen); printf ("sending to %s\n" , sock_ntop_host(sa, salen)); sendto(sockfd, "" , 1 , 0 , sa, salen); n = recvfrom(sockfd, recvline, MAXLINE, 0 , NULL , NULL ); recvline[n] = '\0' ; fputs (recvline, stdout ); exit (0 ); }
11.15 udp_connect函数 udp_connect函数创建一个已连接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 #include "unp.h" int udp_connect (const char * hostname, const char * service) ;int udp_connect (const char * hostname, const char * service) { int sockfd, n; struct addrinfo hints , *res , *ressave ; bzero(&hints, sizeof (struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((n = getaadrinfo(host, serv, &hints, &res)) != 0 ) err_quit(......); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktyep, res->ai_protocol); if (sockfd < 0 ) continue ; if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0 ) break ; close(sockfd); } while ((res = res->ai_next) != NULL ); if (res == NULL ) err_sys(....); freeaddrinfo(ressave); return (sockfd); }
与未连接的区别,不需要把addrinfo获取的对端地址返回出去了,连接后,后续只需要通过write和read即可
11.16 udp_server函数 用于简化访问getaddrinfo的最后一个UDP接口函数是udp_server
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 #include "unp.h" int udp_server (const char * hostname, const char * service, socklen_t *lenp) ;int udp_server (const char * hostname, const char * service, socklen_t *lenp) { int sockfd, n; struct addrinfo hints , *res , *ressave ; bzero(&hints, sizeof (struct addrinfo)); hints.ai_flgas = AI_PASSIVE; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; if ((n = getaadrinfo(hotsname, service, &hints, &res)) != 0 ) err_quit(...); ressave = res; do { sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); if (sockfd < 0 ) continue ; if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0 ) break ; close(sockfd); } while ((res = res->ai_next) != NULL ); if (res == NULL ) err_sys(...); if (addrlenp) *addrlenp = res->ai_addrlen; freeaddrinfo(ressave); return (sockfd); } #include <time.h> #include "unp.h" int main (int argc, char **argv) { int sockfd; ssize_t n; time_t ticks; char buff[MAXLINE]; socklen_t len; struct sockaddr_storage cliaddr ; if (argc == 2 ) sockfd = udp_server(NULL , argv[1 ], NULL ); else if (argc == 3 ) sockfd = udp_server(argv[1 ], argv[2 ], NULL ); else err_quit("usage: daytimeudpserv [ <host> ] <service or port> " ); for (;;) { len = sizeof (cliaddr); n = recvfrom(sockfd, buff, MAXLIEN, 0 , (struct sockaddr*)&cliaddr, &lne); printf ("datagram from %s\n" , sock_ntop((struct sockaddr*)&ciladdr, len)); ticks = time(NULL ); snprintf (buff, sizeof (buff), "%.24s\r\n" , ctime(&ticks)); sendto(sockfd, buff, strlen (buff), 0 , (struct sockaddr*)&cliaddr, len); } }
11.17 getnameinfo 函数 getnameinfo是getaddrinfo的互补函数:它以一个套接口地址为参数,返回描述其中的主机的一个字符串和描述其中服务的另一个字符串。本函数以协议无关的方式提供信息
1 2 3 4 5 #include <netdb.h> int getnameinfo (const struct sockaddr *sockaddr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags) ;
addrlen参数的长度,通常由accept,recvfrom,getsockname或getpeername返回
sock_ntop和getnameinfo的区别 :前者不涉及DNS,单纯返回IP地址和端口号的一个可显示版本,后者通常尝试获取主机和服务的名字
flags的六个标志 :
当知道处理的是数据报套接口时,应设置NI_DGRAM标志,因为getnameinfo无法根据套接口给出的地址和端口号,确定所用协议(tcp or udp)。
设置NI_NAMEREQD,如果无法使用DNS反向解析出主机名,将导致返回一个错误。需要把客户的IP地址映射成主机名的那些服务器可以使用这个特性。
NI_NOFQDN标志导致返回的主机名被截去第一个点号之后的内容。例如aix.unpbook.com,如果设置了本标志,返回的主机名为aix
NI_NUMERICHOST标志告知getnameinfo不要调用DNS,而是以数值表达式格式作为字符串返回IP地址(实现可能为inet_ntop),NI_NUMERICSERV标志指定以十进制数格式作为字符串返回端口号,以代替查找服务名,NI_NUMERICSOPE标志指定以数值格式作为字符串返回范围标识,以代替其名字。
既然客户的端口号通常没有关联的服务名,它们是临时端口,服务器通常应该设置NI_NUMERICSERV
11.18 可重入函数 首先指出gethostbyname函数是不可重入函数。原因如下
参考之前APUE对不可重入函数的定义:它们使用静态数据结构
很明显,static struct hostent host 符合这一条件
在一个普遍的UNIX进程中发生重入问题的条件是:从它的主控制流中和某个信号处理函数中同时调用不可重入函数(比如此处的gethostbyname或gethostbyaddr)。考虑如下例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 main () { struct hostent *hptr ; ... signal(SIGALRM, sig_alrm); ... hptr = gethostbyname(...); ... } void sig_alrm (int signo) { struct hostent *hptr ; ... hptr = gethostbyname(...); ... }
如果主控制流被暂停时正处于执行gethostbyname期间(比如已经填写好host变量准备返回),由于随后信号处理函数再一次调用gethostbyname,该host变量被重用,原先主控制流计算出的值被重写成了信号处理函数调用计算出的值。
同理ernno变量,存在同样的问题,比如如下例子:
1 2 3 4 if (close(fd) < 0 ) { fprintf (stderr , "close error, errno = %d\n" , errno); exit (1 ); }
如果输出errno的值之前,一个信号处理函数被执行,同时执行另一个系统调用发生错误,则由close设置的errno值被覆写
一种解决方法
在信号处理函数,提前保存,返回之前在恢复
1 2 3 4 5 6 7 void sig_alrm (int signo) { int errno_save; errno_save = errno; if (write(...) != nbytes) fprintf (stderr , "write error, errno = %d\n" , errno); errno = errno_save; }
11.19 gethostbyname_r 和 gethostbyaddr_r 函数 两种将诸如gethostbyname之类不可重入函数改为可重入函数的方法
把由不可重入函数填写并返回静态结构的做法改为由调用者分配再由可重入函数填写结构。比如对于gethostbyname来说,调用者需要提供一个填写hostent结构的指针,存放其他信息所用缓冲区,以及该缓冲区大小,以及h_errnop变量存放错误码
由可重入函数调用malloc ( ? )
1 2 3 4 5 6 7 8 #include <netdb.h> struct hostent * gethostbyname_r (const char * hostname, struct hostent * result, char * buf, int bufflen, int *h_errnop) ;struct hostent * gethostbyaddr_r (const char * addr, int len, int type, struct hostent * result, char * buf, int bufflen, int *h_errnop) ;
每个函数都需要4个额外的参数。result参数指向调用者分配并由被调用函数填写的hostent结构
buf参数指向由调用者分配且大小为buflen的缓冲区。如果出错,错误通过h_errnop返回
11.20 作废的IPV6地址解析函数 在开发IPV6期间,用于查找IPV6地址的API经历了若干次反复。最终在RFC 3493中被简单替换成getaddrinfo和getnameinfo。
RES_UES_INET6常值
gethostbyname2函数
1 2 3 #include <netdb.h> struct hostent * gethostbyname2 (const char * name, int af) ;
当af参数为AF_INET时,gethostbyname2的行为与gethostbyname一样,即查找并返回IPV4地址。当af参数为AF_INET6时,gethostbyname2只查找AAAA记录并返回IPV6地址
getipnodebyname函数
1 2 3 4 #include <netdb.h> struct hostent * getipnodebyname (const char * name, int af, int flags, int *error_num) ;
af和flags参数映射到getaadrinfo的hints.ai_family和hints.ai_flags参数。
为了线程安全,返回值是动态的,因而必须使用freehostent函数释放
1 2 #include <netdb.h> void freehostent (struct hostent *ptr) ;
getidnodebyname和与之匹配的getipnodebyaddr函数被 RFC 3493废除,并代之以getaddrinfo和getnameinfo函数
11.21 其他网络相关信息 本章主讲主机名和IP地址以及服务名和端口号。总的来看,应用进程可能想要查找四类与网络有关的信息:主机,网络,协议和服务。比如针对主机的gethostbyname和gethostbyaddr,针对服务的getservbyname和getservbyport
每类信息都定义了各自的结构,包括:hostent,netent,protoent和servent
只有主机和网络信息可通过DNS获取,协议和服务信息总是从相应的文件中读取。
11.22 小结 应用程序用来把主机名转换成IP地址或做相反转换的一组函数被称为解析器。gethostbyname和gethostbyaddr时解析器历史性的入口点。随着IPV6和线程化编程的模型的转移,getaddrinfo和getnameinfo显得更为有用,因为它们既能解析IPV6地址,又符合线程安全。
第14章 高级I/O函数 14.1 概述 在I/O操作上设置超时,有三种方法。read和write这两个函数的三个变体:recv和send(允许通过从它们的第4个参数从进程到内核传递标志),readv和writev以及recvmsg和sendmsg
考虑如何确定套接口接收缓冲区中的数据量,如何在套接口上使用C的标准I/O函数库
14.2 套接口超时 在涉及套接口I/O操作上设置超时的三种方法:
调用alarm,产生信号,利用信号处理打断系统调用
调用select,select有内置的时间限制
套接口选项,SO_RCVTIMEO和SO_SNDTIMEO。但并非所有实现都支持这两个套接口选项
前两个技术适用于任何描述字,而第三个技术仅仅适用于套接口描述字,且对connect不适用
使用SIGALRM为connect设置超时
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 #include "unp.h" static void connect_alarm (int ) ;int connect_timeo (int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec) { Sigfunc *sigfunc; int n; sigfunc = Signal(SIGALRM, connect_alarm); if (alarm(nsec) != 0 ) err_msg("connect_timeo: alarm was alrady set" ); if ((n = connect(sockfd, saptr, salen)) < 0 ) { close(sockfd); if (errno == EINTR) errno = ETIMEDOUT; } alarm(0 ); Signal(SIGALRM, sigfunc); return (n); } static void conncet_alarm (int signo) { return ; }
两点问题:
1.由于connect的超时通常为75s,此技术可以指定比75s小的值,但指定大于75的值,conncet仍将在75s超时
2.存在一些系统调用返回EINTR时,会重新执行同一个系统调用
使用SIGALRM为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 #include "unp.h" static void sig_alrm (int ) ;void dg_cli (FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE]; Signal(SIGALRM, sig_alrm); while (Fgets(sendline, MAXLINE, fp) != NULL ) { Sendto(sockfd, sendline, strlen (sendline), 0 , pservaddr, servlen); alarm(5 ); if ((n = recvfrom(sockfd, recvline, MAXLINE, 0 , NULL , NULL )) < 0 ) { if (errno == EINTR) fprintf (stderr , "socket timeout\n" ); else err_sys("recvfrom error" ); } else { alarm(0 ); recvline[n] = 0 ; fputs (recvline, stdout ); } } } static void sig_alrm (int signo) { return ; }
使用select为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 #include "unp.h" int readabl_timeo (int fd, int sec) { fd_set rset; struct timeval tv ; FD_ZERO(&rset); FD_SET(fd, &rset); tv.tv_sec = sec; tc.tv_usec = 0 ; return (select(fd+1 , &rset, NULL , NULL , &tv)); } #include "unp.h" void dg_cli (FILE *fp, int sockfd, const struct sockaddr* pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE+1 ]; while (Fgets(sendline, MAXLINE, fp) != NULL ) { Sendto(sockfd, sendline, strlen (sendline), 0 , pservaadr, servlen); if (Readable_timeo(sockfd, 5 ) == 0 ) { fprintf (stderr , "socket timeout\n" ); } else { n = Recvfrom(sockfd, recvline, MAXLIEN, 0 , NULL , NULL ); recvline[n] = 0 ; Fputs(recvline, stdout ); } } }
使用SO_RCVTIMEO套接口选项为recvfrom设置超时
SO_RCVTIMEO仅适用于读操作,SO_SNDTIMEO仅适用与写操作
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 #include "unp.h" void dg_cli (FILE *fp, int sockfd, const struct sockaddr *pservaddr, socklen_t servlen) { int n; char sendline[MAXLINE], recvline[MAXLINE]; struct timeval tv ; tv.tv_sec = 5 ; tc.tv_usec = 0 ; Setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof (tv)); while (Fgets(sendline, MAXLINE, fp) != NULL ) { Sendto(sockfd, sendline, strlen (sendline), 0 , pservaddr, servlen); n = recvfrom(sockfd, recvline, MAXLINE, 0 , NULL , NULL ); if (n < 0 ) { if (errno == EWOULDBLOCK) { fprintf (stderr , "socket timeout\n" ); continue ; } else err_sys("recvfrom error" ); } recvline[n] = 0 ; Fputs(recvline, stdout ); } }
14.3 recv和send函数 类似标准read和write函数,不过需要一个额外的参数
1 2 3 4 #include <sys/socket.h> ssize_t recv (int sockfd, void *buff, size_t nbytes, int flags) ;ssize_t send (int sockfd, const void *buff, size_t nbytes, int flags) ;
flags的参数要么为0,要么为下值
flags参数设计成值传递,而不是值-结果参数,因此只能从进程向内核,而不能把内核的标志传回来。后续提出了这个需求,但没有改变这个函数的参数,将这个需求加入了recvmsg和sendmsg所用的msghdr结构。该结构新增了一个整数msg_flags成员。
14.4 readv和writev函数 readv和writev允许单个系统调用读入到或写出自一个或多个缓冲区。分散读和集中写,将读操作的数据分散到多个应用缓冲区,将多个应用缓冲区的数据提供给单个写操作
1 2 3 4 5 6 7 8 9 10 #include <sys/uio.h> ssize_t readv (int filedes, const struct iovec *iov, int iovcnt) ;ssizr_t writev (int filedes, const struct iovec *iov, int iovcnt) ; struct iovec { void *iov_base; size_t iov_len; };
第2个参数指向某个iovec结构数组的一个指针,iovcnt应该是这个数组元素的数量
readv和writev这两个函数可用于任何描述字,而不仅限于套接口。另外writev是一个原子操作
14.5 recvmsg和sendmsg函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <sys/socket.h> ssize_t recvmsg (int sockfd, struct msghdr *msg, int flags) ;ssize_t sendmsg (int sockfd, struct msghdr *msg, int flags) ; struct msghdr { void *msg_name; socklen_t msg_namelen; struct iovec *msg_iov ; int msg_iovlen; void *msg_control; socklen_t msg_controllen; int msg_flags; }
msg_name和msg_namelen参数,类似于recvfrom和sendto的第5和第6参数,适用于套接口未连接的场合(如未连接UDP套接口)。对于已连接UDP套接口或TCP套即口,应置为空指针
msg_iov和msg_iovlen参数,类似readv和writev的第2和第3参数
msg_contorl和msg_controllen指定可选的辅助数据的位置和大小。见14.6
对于函数参数中的flags和结构中的msg_flags参数,flags参数由sendmsg直接使用,sendmsg忽略msg_flags成员。msg_flags成员由recvmsg使用,recvmsg调用时,flags参数被拷贝到msg_flags成员。内核还依据recvmsg的结果更新其值。
flags参数解释见p336
14.6 辅助数据 辅助数据可通过调用sendmsg和recvmsg这两个函数,使用msghdr结构中的msg_control和msg_controllen这两个成员发送和接收。
辅助数据的各种用途见下图:
1 2 3 4 5 6 7 #include <sys/socket.h> struct cmsghdr { socklen_t cmsg_len; int cmsg_level; int cmsg_type; };
由于recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充文字,头文件种定义了以下5个宏,以简化对辅助数据的对处理
1 2 3 4 5 6 7 8 9 10 11 12 #include <sys/socket.h> #include <sys/param.h> struct cmsghdr * CMSG_FIRSTHDR (struct msghdr *mhdrptr) ; struct cmsghdr * CMSG_NXTHDR (struct msghdr *mhdrptr, struct cmsghdr *cmsgptr) ; unsigned char * CMSG_DATA (struct cmsghdr *cmsgptr) ; unsigned int CMSG_LEN (unsigned int lenght) ; unsigned int CMSG_SPACE (unsigned int lenght) ;
CMSG_LEN和CMSG_SPACE的区别:前者不考虑数据部分之后可能填充的字节,返回的时存放在cmsg_len中的值,后者计上结尾处可能的填充字节
14.7 排队的数据量 有时我们想要在不真正读取数据的前提下直到一个套接口上已有多少数据排队等着读取。
如果我们获悉已排队数据量的目的在于避免读操作阻塞在内核中,可以使用非阻塞I/O
如果我们既想查看数据,又想数据留在接收队列中以供本进程其他部分稍后读取,可以使用MSG_PEEK标志(图14.6)
一些实现支持ioctl的FIONREAD命令。该命令的第3个ioctl参数是指向某个整数的一个指针,内核通过该整数返回的值就是套接口接收队列的当前字节数。
p342
14.8 套接口和标准I/O 标准I/O函数库可用于套接口,不过需要考虑以下几点:
通过调用fdopen函数,为描述字创建一个标准I/O流。
TCP和UDP套接口是全双工的。标准I/O流也是,只要以r+类型打开流即可。但是在这样的流上,输入和输出操作之间必须要有fflush,fseek,fsetpos或rewind其中一个,但问题是,它们都会调用lseek,而lseek在套接口上会失败
解决上述问题最简单的方法,为一个给定套接口,打开两个标准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 #include "unp.h" void str_echo (int sockfd) { char line[MAXLINE]; FILE *fpin, *fpout; fpin = Fdopen(sockfd, "r" ); fpout = Fdopen(sockfd, "w" ); while (Fgets(lien, MAXLINE, fpin) != NULL ) Fputs(line, fpout); } hputx % tcp cli02 206.168 .112 .96 hello, world 键入本行,但无回射输出 and hi 再键入本行,仍无回射输出 hello?? 再键入本行,仍无回射输出 ^D 键入EOF字符 hello,world 至此才输出那三个回射行 and hi hello??
问题出去,标准I/O执行缓冲的策略上。标准I/O执行以下三类缓冲:
完全缓冲:缓冲区满,进程显示调用fflush或者进程调用exit终止自身
行缓冲:碰到一个换行符,进程调用fflush或者进程调用exit终止自身
不缓冲:每次调用标准I/O都发生I/O
标准I/O函数库的大多数Unix实现使用如下规则:
标准错误输出总是不缓冲
标准输入和标准输出是完全缓冲,除非它们指代终端设备,这种情况下行缓冲
所有其他I/O流都是完全缓冲,除非它们指代终端设备,这种情况下行缓冲
套接口不是终端设备,所以完全缓冲。解决方法:1.调用setvbuf迫使这个输出流变成行缓冲。2.显示调用fflush
14.9 高级轮询技术 /dev/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 struct dvpoll { struct pollfd *dp_fds ; int dp_nfds; int dp_timeout; }; dp_fds指向一个缓冲区,供ioctl返回时存放一个pollfd结构数组。 dp_nfds指定该缓冲区的大小 dp_timeout指定超时,非阻塞,和不设置超时(一直阻塞) #include "unp.h" #include <sys/devpoll.h> void str_cli (FILE *fp, int sockfd) { int stdineof; char buf[MAXLINE]; int n; int wfd; struct pollfd pollfd [2]; struct dvpoll dopoll ; int i; int result; wfd = Open("/dev/poll" , O_RDWR, 0 ); pollfd[0 ].fd = fileno(fp); pollfd[0 ].events = POLLIN; pollfd[0 ].revents = 0 ; pollfd[1 ].fd = sockfd; pollfd[1 ].events = POLLIN; pollfd[1 ].revents = 0 ; Write(wfd, pollfd, sizeof (struct pollfd)*2 ); stdineof = 0 ; for (;;) { dopoll.dp_timeout = -1 ; dopoll.dp_nfds = 2 ; dopoll.dp_fds = pollfd; result = Ioctl(wfd, DP_POLL, &dopoll); FOR (i = 0 ; i < result; i++) { if (dopoll.dp_fds[i].fd == sockfd) { ... ... ... } else { ... ... ... } } } }
kqueue接口
kqueue是一个用于异步事件通知的系统调用,最初由FreeBSD开发。它可以监视文件描述符、定时器和信号等事件,并在这些事件发生时通知进程。相比于传统的轮询方式,kqueue能够提供更高效的事件处理机制 – GPT3
本接口允许进程向内核注册描述所关注kqueue时间的事件过滤器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <sys/types.h> #include <sys/event.h> #include <sys/time.h> int kqueue (void ) ;int kevent (int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents. const struct timespec *timeout) ;void EV_SET (struct kevent *kev, uintptr_t ident, short filter, u_short flags, u_int fflags, intptr_t data, void *udata) ;struct kevent { uintptr_t ident; short filter; u_short flags; u_int fflags; intptr_t data; void *udata; };
kevent函数通过eventlist参数返回
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 #include "unp.h" void str_cli (FILE *fp, int sockfd) { int kq, i, n, nev, stdineof = 0 , isfile; char buf[MAXLINE]; struct kevent kev [2]; struct timespec ts ; struct stat st ; isfile = ((fstat(fileno(fp), &st) == 0 ) && (st.st_mode & S_IFMT) == S_IFREG); EV_SET(&kev[0 ], fileno(fp), EVFILT_READ, EV_ADD, 0 , 0 , NULL ); EV_SET(&kev[1 ], sockfd, EVFILT_READ, EV_ADD, 0 , NULL ); kq = Kqueue(); ts.tv_sec = ts.tv_nsec = 0 ; Kevent(kq, kev, 2 , NULL , 0 , &ts); for (;;) { nev = Kevent(kq, NULL , 0 , kev, 2 , NULL ); for (i = 0 ; i < nev; i++) { if (kev[i].ident == sockfd) { if ((n = read(sockfd, buf, MAXLINE)) == 0 ) { if (stdineof == 1 ) return ; else err_quit("str_cli: server terminated prematurely" ); } write(fileno(stdout ), buf, n); } if (kev[i].ident == fileno(fp)) { n = read(fileno(fp), buf, MAXLINE); if (n > 0 ) writen(sockfd, buf, n); if (n ==0 || (isfile && n == kev[i].data)) { stdineof = 1 ; shutdown(sockfd, SHUR_WR); kev[i].flags = EV_DELETE; Kevent(kq, &kev[i],1 , NULL , 0 , &ts); continue ; } } } } }
14.10 T/TCP:事务目的TCP T/TCP是对TCP的一个略微修改版本,能够避免近来彼此通信过的主机之间的三路握手。
为了处理T/TCP,套接口API需做些变动:
客户调用sendto把数据的发送结合到连接的建立之中。该调用替换分离的connect和write调用。服务器的协议地址改为传递给sendto而不是connect
新增一个输出标志MSG_EOF,用于指示本套接口上不再有数据待发送。该标志允许我们把shutdown调用结合到输出操作(sendto或send)之中。给一个sendto调用同时指定本标志和服务器的协议地址有可能导致发送单个含有SYN,FIN和数据的分节。使用send而不是write也是为了设置该标志。
新定义一个级别为IPPROTO_TCP的套接口选项TCP_NOPUSH。放置TCP为腾空套接口发送缓冲区而发送分节。当某个客户准备以单个sendto发送一个请求,该请求大小超过MSS时,就需要设置本选项。
想使用T/TCP建立连接的化,客户应调用socket, setsockopt(开启TCP_NOPUSH选项)和sendto(若只有一个请求待发送则指定MSG_EOF标志)。如果setsockopt返回ENOPROTOOPT错误或者sendto返回ENOTCONN错误,那么本机不支持T/TCP。这种情况下考虑直接使用普通的TCP,connect+write
服务器的唯一变动,如果服务器想随应答一起发送FIN,它应该指定MSG_EOF标志调用send以发送,而不是调用write发送应答。
14.11 小结 在套接口上设置时间限制的三种方法
五组I/O函数:read/write , recvfrom/sendto, recv/send, recvmsg/sendmsg, readv/writev。本节主讲了后三种
10种不同的辅助数据。
标准I/O函数库在套接口上的使用
高级轮询技术,kqueue和/dev/poll接口
TCP的一个简单增强版,T/TCP,避免三路握手,减少了分节数量
第16章 非阻塞I/O 16.1 概述 套接口缺省是阻塞的。意味着发出一个不能立即完成的套接口调用时,其进程将被投入睡眠,等待响应操作完成。可能阻塞套接口的调用分为以下四类:
1.输入操作:read, readv, recv, recvfrom 和 recvmsg5个函数。对于非阻塞的套接口,如果输入操作不能被满足,相应调用将立即返回一个EWOULDBLOCK错误。
2.输出操作:write, writev, send, sendto 和 sendmsg5个函数。对于阻塞的套接口,如果其发送缓冲区种没有空间,进程将被投入睡眠,直到有空间为止。对于非阻塞的套接口,如果没有空间,立即返回一个EWOULDBLOCK错误。
3.接受外来连接:accept函数。对阻塞调用,并且无新连接到达,调用进程将睡眠。对非阻塞调用,五新连接到达,立即返回一个EWOULDBLOCK错误。
4.发起外出连接:connect函数。由于三路握手需求等待对端的ACK应答,所以connect总是阻塞调用进程至少一个到服务器的RTT时间。如果对非阻塞TCP套接口调用,并且连接不能立即建立,连接的建立照样发起,不过返回一个EINPROGRESS错误。可以立即建立的连接:通常发生在服务器和客户处于同一个主机的情况。
16.2 非阻塞读和写:str_cli函数(修订版) 维护两个缓冲区:
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 #include <unistd.h> #include <sys/socket.h> #include <sys/select.h> #include <netinet/in.h> #include <fcntl.h> #include <sys/time.h> #include <time.h> #include <string.h> #include <stdio.h> #define MAXLINE 1024 char * gf_time () { struct timeval tv ; static char str[30 ]; char *ptr; if (gettimeofday(&tv, NULL ) < 0 ) err_sys("gettimeofday error" ); ptr = ctime(&tv.tv_sec); strcpy (str, &ptr[11 ]); snprintf (str+8 , sizeof (str)-8 , ".%06ld" , tv.tv_usec); return (str); } void str_cli (FILE *fp, int sockfd) { int maxfdp1, val, stdineof; ssize_t n, nwritten; fd_set rset, wset; char to[MAXLINE], fr[MAXLINE]; char *toiptr, *tooptr, *friptr, *froptr; val = fcntl(sockfd, F_GETFL, 0 ); fcntl(sockfd, F_SETFL, val | O_NONBLOCK); val = fcntl(STDIN_FILENO, F_GETFL, 0 ); fcntl(STDIN_FILENO, F_SETFL, val | O_NONBLOCK); val = fcntl(STDOUT_FILENO, F_GETFL, 0 ); fcntl(STDOUT_FILENO, F_SETFL, VAL | O_NONBLOCK); toiptr = tooptr = to; friptr = froptr = fr; stdineof = 0 ; maxfdp1 = max(max(STDIN_FILENO, STDOUT_FILENO), sockfd) + 1 ; while (1 ) { FD_ZERO(&rset); FD_ZERO(&wset); if (stdineof == 0 && toiptr < &to[MAXLINE]) FD_SET(STDIN_FILENO, &rset); if (friptr < &fr[MAXLINE]) FD_SET(sockfd, &rset); if (tooptr != toiptr) FD_SET(sockfd, &wset); if (froptr != friptr) FD_SET(STDOUT_FILENO, &wset); select(maxfdp1, &rset, &wset, NULL , NULL ); if (FD_ISSET(STDIN_FILENO, &rset)) { if ((n = read(STDIN_FILENO, toiptr, &to[MAXLINE]-toiptr)) < 0 ) { if (errno != EWOULDBLOCK) err_sys("read error on stdin" ); } else if (n == 0 ) { fprintf (stderr , "%s: EOF on stdin\n" , gf_time()); stdineof = 1 ; if (tooptr == toiptr) shutdown(sockfd, SHUT_WR); } else { fprintf (stderr , "%s: read %d bytes from stdin\n" , gf_time(), n); toiptr += n; FD_SET(sockfd, &wset); } } if (FD_ISSET(sockfd, &rset)) { if ((n = read(sockfd, friptr, &fr[MAXLINE]-friptr)) < 0 ) { if (errno != EWOULDBLOCK) err_sys("read error on socket" ); } else if (n == 0 ) { fprintf (stderr , "EOF on socket" ); if (stdineof) return ; else err_quit("str_cli:server terminated prematurely" ); } else { fprintf (stderr , "%s:read %d bytes from socket\n" , gf_time(), n); friptr += n; FD_SET(STDOUT_FILENO, &wset); } } if (FD_ISSET(STDOUT_FILENO, &wset) && ((n = friptr - froptr) > 0 )) { if ((nwritten = write(STDOUT_FILENO, froptr, n)) < 0 ) { if (errno != EWOULDBLOCK) err_sys("write error on stdout" ); } else { fprintf (stderr , "%s: wrote %d bytes to stdout\n" , gf_time(), nwritten); froptr += nwritten; if (froptr == friptr) froptr = friptr = fr; } } if (FD_ISSET(sockfd, &wset) && ((n = toiptr - tooptr) > 0 )) { if ((nwritten = write(sockfd, tooptr, n)) < 0 ) { if (errno != EWOULDBLOCK) err_sys("write error to socket" ); } else { fprintf (stderr , "%s: wrote %d bytes to socket\n" ,gf_time(), nwritten); tooptr += nwritten; if (tooptr == toiptr) { toiptr = tooptr = to; if (stdineof) shutdown(sockfd, SHUT_WR); } } } } }
str_cli的较简单版本
与代码的复杂性相比,使用非阻塞I/O的方式不值得。当需要使用非阻塞I/O时,更简单的办法通常是把应用程序任务划分到多个进程(fork或多线程)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "unp.h" void str_cli (FILE *fp, int sockfd) { pid_t pid; char sendline[MAXLINE], recvline[MAXLINE]; if ((pid = fork()) == 0 ) { while (readline(sockfd, recvline, MAXLINE) > 0 ) fputs (recvline, stdout ); kill(getppid(), SIGTERM;) exit (0 ); } while (fgets(sendline, MAXLINE, fp) != NULL ) writen(sockfd, sendline, strlen (sendline)); shutdown(sockfd, SHUT_WR); pause(); return ; }
str_cli执行时间
``
16.3 非阻塞connect 当一个非阻塞的TCP套接口上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的三路握手继续进行。接着使用select检测这个连接或成功或失败的已建立条件。非阻塞connect的三个用途:
非阻塞connect需要处理的细节:
如果连接的服务器在同一个主机上,调用connect时,连接通常立刻建立,需要处理这种情况
关于select和非阻塞connect的以下两个规则 a) 当连接建立成功时,描述字变为可写 b) 当连接建立遇到错误时,描述字变为既可读又可写
16.4 非阻塞connect:时间获取客户程序 将图1.5的connect调用替换成 if (connect_nonb(sockfd, (SA*)&servaddr, sizeof(servaddr),0) <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 #include "unp.h" typedef struct sockaddr SAint connect_nonb (int sockfd, const SA *saptr, socklen_t salen, int nsec) { int flags, n, error; socklen_t len; fd_set rset, wset; struct timeval tval ; flags = fcntl(sockfd, F_GETFL, 0 ); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK); error = 0 ; if ((n = connect(sockfd, saptr, salen)) <0 ) if (errno != EINPROGRESS) return (-1 ); if (n == 0 ) goto done; FD_ZERO(&rset); FD_SET(sockfd, &rset); wset = rset; tval.tv_Sec = nsec; tval.tv_usec = 0 ; if ((n = select(sockfd+1 , &rset, &wset, NULL , nsec ? &tval : NULL )) == 0 ) { close(sockfd); errno = ETIMDOUT; return (-1 ); } if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) { len = sizeof (error); if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0 ) return (-1 ); } else err_quit("select error:sockfd not set" ); done: fcntl(sockfd, F_SETFL, flags); if (error) { close(sockfd); errno = error; return (-1 ); } return 0 ; }
被中断的connect
对于一个正常的阻塞式套接口,如果其上的connect调用在TCP三路握手中被中断。假设被中断的connect调用不由内核自动重启,它将返回EINTR。我们不能再次调用connect等待未完成的连接继续完成。这样做将导致返回EADDRINUSE错误。
这种情况下只能调用select,就像本节这样对于非阻塞connect所作的那样。连接建立成功时select返回套接口可写条件,连接建立失败时select返回套接口既可读又可写条件。
16.5 非阻塞connect: Web客户程序 web程序出自13.4节,13章跳过了,本小节也先跳过。
16.6 非阻塞accept 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 #include <sys/socket.h> #include <sys/select.h> #include <unistd.h> #include <types.h> #include <stdio.h> #include <errno.h> #include <strings.h> #include <netinet/in.h> #include <arpa/inet.h> #define SERV_PORT 7777 int main (int argc, char **argv) { int sockfd; struct linger ling ; struct sockaddr_in servaddr ; if (argc != 2 ) perror("usage: tcpcli<IPaddress> " ); sockfd = socket(AF_INET, SOCK_STREAM, 0 ); bzero(&servaddr, sizeof (servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1 ], &servaddr.sin_addr); connect(sockfd, (struct sockaddr*)&servaddr, sizeof (servaddr)); ling.l_onoff = 1 ; ling.l_linger = 0 ; setsockopt(sockfd, SOL_SOCKET, SO_LINGER, &ling, sizeof (ling)); close(sockfd); exit (0 ); }
该程序的目的在于,建立连接后,发送一个RST,随后关闭该套接口
接着修改服务器程序部分:
1 2 3 4 5 6 7 if (FD_ISSET(listenfd, &rset)) {+ printf ("listening socket readable\n" ); + sleep(5 ); clilen = sizeof (cliaddr); connfd = accept(listenfd, (struct sockaddr*)&cliaddr, clilen); ... }
+行为图6.21和图6.22不同的新增行,意在模拟一个繁忙的TCP服务器,该服务器无法在收到select的可读条件后立马调用accept,结合上图发送一个RST的TCP回射客户程序,就会出现如下情况:
客户如上图,建立一个连接并随后夭折它
select像服务器进程返回到调用accept期间,服务器TCP收到来自客户的RST
这个已完成的连接被服务器TCP驱除出队列,假设队列中没有其他已完成的连接
服务器调用accept,但是由于没有任何已完成的连接,服务器于是阻塞。
服务器会一直阻塞在accept调用上,直到其他客户建立连接位置。在此期间,服务器无法处理任何其他已就绪的描述字,因为被accept调用所阻塞。
解决方法如下:
当使用select获悉某个监听套接口上何时有已完成的连接准备好被accept时,将这个监听套接口设置为非阻塞。
在后续的accept调用中忽略以下错误:EWOULDBLOCK,ECONNABORTED,EPROTO,EINTR
16.7 小结 select结合非阻塞I/O一起使用,以便判断描述字何时可写可读。由此写出了所有str_cli版本中,执行速度最快的,但其代码同样也是最复杂的。使用fork,来替代非阻塞I/O是个更好的选择。
非阻塞connect使得我们能够在TCP三路握手之间,做其他处理,而不光是阻塞在connect上。但非阻塞connect不可移植,不同的实现有不同的手段指示连接建立已成功完成或已碰到错误。使用非阻塞connect开发了一个新型客户程序,Web客户程序.
第26章 线程 26.1 概述 详细的在apue里已经讲过了,这节算是线程方面的一些基础,线程的创建和销毁设置分离,简单的线程控制,互斥锁和条件变量的使用,以及使用线程代替非阻塞connect
26.2 基本线程函数: 创建和终止 pthread_create函数
1 2 3 4 #include <pthread.h> int pthread_create (pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg) ;
tid 返回创建的线程id,attr为线程属性,func线程处理函数,arg传递给线程处理函数的参数
线程属性等详细见apue
pthread_join函数
等待一个给定线程终止。对比线程,pthread_create类似于fork,pthread_join类似于waitpid
1 2 3 #include <pthread.h> int pthread_join (pthread_t tid, void **status) ;
status为线程返回值
pthread_self函数
1 2 3 #include <pthrad.h> pthread_t pthread_self (void ) ;
类似于getpid
pthread_detach函数
1 2 3 #include <pthread.h> int pthread_detach (pthread_t tid) ;
简单来说,设置线程分离后,该线程不需要我们在手动回收(pthread_join),线程终止时,会将相关资源都释放
pthread_exit函数
1 2 3 #include <pthread.h> void pthread_exit (void *status) ;
让一个线程终止的方法之一。
让一个线程终止的另两种方法:
启动线程的函数可以返回。
进程main函数返回或者任何线程调用exit,整个进程终止。
26.3 使用线程的str_cli函数 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 #include "unpthread.h" void * copyto (void *) ;static int sockfd; static FILE *fp;void str_cli (FILE *fp_arg, int sockfd_arg) { char recvline[MAXLINE]; pthread_t tid; sockfd = sockfd_arg; fp = fp_arg; pthread_create(&tid, NULL , copyto, NULL ); while (Readline(sockfd, recvline, MAXLINE) > 0 ) Fputs(recvline, stdout ); } void * copyto (void *arg) { char sendline[MAXLINE]; while (Fgets(sendline, MAXLINE, fp) != NULL ) Writen(sockfd, sendline, strlen (sendline)); Shutdonw(sockfd, SHUT_WR); return (NULL ); }
26.4 使用线程的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 #include "unpthread.h" static void * doit (void *) ;int main (int argc, char **argv) { int listenfd, connfd; socklen_t addrlen, len; struct sockaddr *cliaddr ; pthread_t tid; if (argc == 2 ) listenfd = Tcp_listen(NULL , argv[1 ], &addrlen); else if (argc == 3 ) listenfd = Tcp_listen(argv[1 ], argv[2 ], &addrlen); else err_quit("usage: tcpserv01 [<host>] <service or port> " ); cliaddr = Malloc(addrlen); for (;;) { len = addrlen; connfd = Accept(listenfd, cliaddr, &len); Pthread_create(&tid, NULL , &doit, (void *)connfd); } } static void * doit (void *arg) { Pthread_detach(pthread_self()); str_echo((int )arg); CLose((int )arg); return (NULL ); }
给线程传递参数
注意不能简单地把connfd的地址传递给新线程,如下代码所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int main (int argc, char **argv) { ... int listenfd, connfd; ... for (;;) { len = addrlen; connfd = Accept(listenfd, cliaddr, &len); Phthrea_create(&tid, NULL , &doit, &connfd); } } static void * doit (void *arg) { int connfd; connfd = *((int *)arg); Pthread_detach(pthread_self()); str_echo(connfd); Close(connfd); return NULL ; }
由于connfd指向同一块地址,connfd的值会受Accept的影响发生变动,要么使用最开始的直接传值的方式,要么对每个线程分配一块空间,再由线程释放
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 #include "unpthread.h" static void * doit (void *) ;int main (...) { int listenfd, connfd; thread_t tid; socklen_t addrlen, len; struct sockaddr *cliaddr ; if (argc == 2 ) .... cliaddr = Malloc(addrlen); for (;;) { len = addrlen; iptr = Malloc(sizeof (int )); *iptr = Accept(listenfd, cliaddr, &len); Pthread_create(NULL , NULL , &doit, iptr); } } static void * doit (void *arg) { int connfd; connfd = *((int *)arg); free (arg); Pthread_detach(pthread_self()); str_echo(connfd); Close(connfd); return NULL ; }
但由于引入了malloc和free这两个不可重入函数,产生了线程安全的问题
线程安全函数
26.5 线程特定数据 处理将未线程化程序转换成使用线程的版本时,函数使用静态变量引起错误的问题。
1 2 3 4 #include <pthread.h> int pthread_once (pthread_once_t *onceptr, void (*init)(void )) ;int pthread_key_create (pthread_key_t *keyptr, void (*destructor)(void *value)) ;
简单来说,pthread_key_create用来初始化线程特定数据,在进程范围内对于一个给定键,该函数只能被调用一次。pthread_once则用来确保该键只被调用一次,pthread_key_create在Key结构数组中,找到第一个未引用的元素,将它的索引返回给调用者
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 pthread_key_t r1_key;pthread_once_t r1_once = PTHREAD_ONCE_INIT;void readline_destructor (void *ptr) { free (ptr); } void readline_once (void ) { pthread_key_create(&r1_key, readline_destructor); } ssize_t readline (...) { ... pthread_once(&r1_once, readline_once); if ((ptr = pthread_getspecific(r1_key)) == NULL ) { ptr = Malloc(...); pthread_setspecific(r1_key, ptr); } ... }
pthread_getspecific 和 pthread_setspecific 用于获取和存放相关联的值
1 2 3 4 5 #include <pthread.h> void * pthread_getspecific (pthread_key_t key) ; int pthread_setspecific (pthread_key_t key, const void *value) ;
图3.18函数的优化版本
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 #include "unpthread.h" static pthread_key_t r1_key;static pthread_once_t r1_once = PTHREAD_ONCE_INIT;static void readline_destructor (void *ptr) { free (ptr); } static void read_once (void ) { Pthread_key_create(&r1_key, readline_destructor); } typedef struct { int r1_cnt; char *r1_bufptr; char r1_buf[MAXLINE]; }Rline; static ssize_t my_read (Rline *tsd, int fd, char *ptr) { if (tsd->r1_cnt <= 0 ) { again: if ((tsd->r1_cnt = read(fd, tsd->r1_buf, MAXLINE)) < 0 ) { if (errno == EINTR) goto again; return (-1 ); } else if (tsd->r1_cnt == 0 ) return 0 ; tsd->r1_bufptr = tsd->r1_buf; } tsd->r1_cnt--; *ptr = *tsd->r1_bufptr++; return 1 ; } ssize_t readline (int fd, void *vptr, size_t maxlen) { int n, rc; char c, *ptr; Rline *tsd; Pthread_once(&r1_once, readline_once); if ((tsd = pthread_getspecific(r1_key)) == NULL ) { tsd = Calloc(1 , sizeof (Rline)); Pthread_setspecific(r1_key, tsd); } ptr = vptr; for (n = 1 ; n < maxlen; n++) { if ((rc = my_read(tsd, fd, &c)) == 1 ) { *ptr++ = c; if (c == '\n' ) break ; } else if (rc == 0 ) { *ptr = 0 ; return (n-1 ); } else return -1 ; } *ptr = 0 ; return n; }
26.6 Web客户与同时连接 用线程代替了非阻塞conncet的版本,原版本16.5节
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 #include "unpthread.h" #include <thread.h> #define MAXFILES 20 #define SERV "80" struct file { char *f_name; char *f_host; int f_fd; int f_flags; pthread_t f_tid; } file[MAXFILES]; #define F_CONNECTING 1 #define F_READING 2 #define F_DONE 4 #define GET_CMD "GET %s HTTP/1.0\r\n\r\n" int nconn, nfiles, nlefttoconn, nlefttoread;void *do_get_read (void *) ;void home_page (const char *, const char *) ;void write_get_cmd (struct file*) ;int main (int argc, char **argv) { int i, n, maxnconn; pthread_t tid; struct file *fptr ; if (argc < 5 ) err_quit("..." ); maxnconn = atoi(argv[1 ]); nfiles = min(argc - 4 , MAXFILES); for (i = 0 ; i < nfiles; i++) { file[i].f_name = argv[i+4 ]; file[i].f_host = argv[2 ]; file[i].f_flags = 0 ; } printf ("nfiles = %d\n" , nfiles); home_page(argv[2 ], argv[3 ]); nlefttoread = nlefttoconn = nfiles; nconn = 0 ; while (nlefttoread > 0 ) { while (nconn < maxnconn && nlefttoconn > 0 ) { for (i = 0 ; i < nfiles; i++) if (file[i].f_flags == 0 ) breka; if (i == nfiles) err_quit("nlefttoconn = %d but nothing found" , nlefttoconn); file[i].f_flags = F_CONNECTING; Pthread_create(&tid, NULL , &do_get_read, &file[i]); file[i].f_tid = tid; nconn++; nleftoconn--; } if ((n = thr_join(0 , &tid, (void &&)&fptr)) != 0 ) errno = n, err_sys("..." ); nconn--; nlefttoread--; printf ("thread id %d for %s done\n" , tid, fptr->f_name); } exit (0 ); } void * do_get_read (void *vptr) { int fd, n; char line[MAXLINE]; struct file *fptr ; fptr = (struct file*)vptr; fd = Tcp_connect(fptr->f_host, SERV); fptr->f_fd = fd; printf ("do_get_read for %s, fd %d, thread %d\n" , fptr->f_name, fd, fptr->f_tid); write_get_cmd(fptr); for (;;) { if ((n = Read(fd, line, MAXLINE)) == 0 ) break ; printf ("read %d bytes from %s\n" , n, fptr->f_name); } printf ("end-ofile on %s\n" , fptr->f_name); Close(fd); fptr->f_flags = F_DONE; return (fptr); }
thr_join为一个Solaris线程函数,等待任一线程终止。原因在于Pthreads没有提供等待任一线程终止的手段;pthread_join需要显示指定想要等待的线程。后续将看到使用条件变量替换该线程函数的方法。
26.7 互斥锁 处理线程之间竞争的问题,比如对于一变量arg,线程1使用的时候,可能在使用之前,该值被另一线程更改
1 2 3 4 #include <pthread.h> int pthread_mutex_lock (pthread_mutex_t *mptr) ;int pthread_mutex_unlock (pthread_mutex_t *mptr) ;
如果试图上锁已被另外某个线程锁住的一个互斥锁,本线程将被阻塞,直到该互斥锁被解锁为止
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 "unpthread.h" #define NLOOP 5000 int counter;pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;void * doit (void *) ;int main (...) { pthread_t tidA, tidB; Pthread_create(&tidA, NULL , &doit, NULL ); Pthraed_create(&tidB, NULL , &doit, NULL ); Pthread_join(tidA, NULL ); Pthread_join(tidB, NULL ); exit (0 ); } void * doit (void *vptr) { int i, val; for (i = 0 ; i < NLOOP; i++) { Pthread_mutex_lock(&counter_mutex); val = counter; printf ("%d: %d\n" , pthread_self(), val+1 ); counter = val + 1 ; Pthread_mutex_unlock(&counter_mutex); } return (NULL ); }
本例为一个简单的不同线程对同一个变量递增,如果不采用互斥锁的话,由于竞争问题,会导致递增出现错误
26.8 条件变量 与互斥锁相比,互斥锁是主动上锁,避免竞争,条件变量,等待条件满足,然后处理相关
1 2 3 4 #include <pthread.h> int pthread_cond_wait (pthread_cond_t *cptr, pthread_mutex_t *mptr) ;int pthread_cond_signal (pthread_cond_t *cptr) ;
举例来说明。条件变量的使用,同时也需要用到互斥锁的功能
对于26.6 thr_join的替代
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 int ndone;pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t ndone_cond = PTHREAD_COND_INTIIALIZER;Pthread_mutex_lock(&ndone_mutex); ndone++; Phtread_cond_signal(&ndone_cond); Pthread_mutex_unlock(&ndone_mutex); while (nlefttoread < 0 ) { while (nconn < maxnconn && nlefttoconn >0 ) { ... } Pthread_mutex_lock(&ndond_mutex); while (ndone == 0 ) Pthread_cond_wait(&ndone_con, &ndone_mutex); for (i = 0 ; i < nfiles; i++) { if (file[i].f_flags & F_DONE) { Pthread_join(file[i].f_tid, (void **)&fptr); ... } } Pthread_mutex_unlock(&ndone_mutex); }
pthread_cond_wait上的原子操作:将互斥锁解锁然后把调用线程投入睡眠
另外两个与signal作用类似的
1 2 3 4 #include <pthread.h> int pthread_cond_broadcast (pthread_cond_t *cptr) ;int pthread_cond_timewait (pthread_cond_t *cptr, pthread_mutex_t *mptr, const struct timespec *abstime) ;
一个与signal对于,但是signal是唤醒单个等在相应条件变量上的线程,broadcast唤醒所有。
timewait则是给阻塞设置了一个时间限制,且此处的时间为绝对时间。
26.9 Web客户与同时连接 用上述所讲的方法,替换26.6节中的thr_join函数
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 #define F_JOINED 8 int ndone;pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;printf ("end-of-file on %s\n" , fptr->f_name);Close(fd); Pthread_mutex_lock(&ndone_mutex); fptr->f_flags = F_DONE; ndone++; Pthread_cond_signal(&ndone_cond); Pthread_mutex_unlock(&ndone_mutex); return (fptr);while (nliefttoread > 0 ) { while (nconn < maxnconn && nlefttoconn > 0 ) { for (i = 0 ; i < nfiles; i++) if (file[i].f_flags == 0 ) break ; if (i == nfiles) err_quit("..." ); file[i].f_flags = F_CONNECTING; Pthread_create(&tid, NULL , &do_get_read, &file[i]); file[i].f_tid = tid; nconn++; nlefttoconn--; } Pthread_mutex_lock(&ndone_mutex); while (ndone == 0 ) Pthread_cond_wait(&ndone_cond, &ndone_mutex); for (i = 0 ; i < nfiles; i++) { if (file[i].f_flags & F_DONE) { Pthread_join(file[i].f_tid, (void **)&fptr); if (&file[i] != fptr) err_quit("..." ); fptr->f_flags = F_JOINED; ndone--; nconn--; nlefttoread--; printf ("thread %d for %s done\n" , fptr->f_tid, fptr->f_name); } } Pthread_mutex_unlock(&ndone_mutex); } exit (0 );}
26.10 小结 创建线程比fork更快,即体现线程在繁重使用的网络服务器上的优势。
同一进程内的所有线程共享全局变量和描述字,从而允许不同线程之间共享这些信息,但同时也引入了同步问题。
编写能够被线程化应用程序调用的函数时,这些函数必须做到线程安全。