2026-01-27 00:56:34UNP 学习笔记

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模型:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120//nc 命令 nc + ip + duan'kou'ha//server#include #include #include #include #include #include #include #include #define SERV_IP "172.23.155.242"#define SERV_PORT 6666int 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]; //创建socket 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); //绑定IP和端口 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;}//client#include #include #include #include #include #include #include #include #include #define SERV_IP "172.23.155.242"#define SERV_PORT 6666int main() { int cfd; struct sockaddr_in serv_addr; // socklen_t serv_addr_len; 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可用状态。

2MSLTIME_WAIT状态等待的时间,具体时长取决于实现

TIME_WAIT和2MSL存在的意义,确保最后一次ACK应答被收到

半关闭TCP连接中,A发送FIN请求,进入到FIN_WAIT_1状态,B端回应ACK后,(A端进入FIN_WAIT_2状态),B没立即使,发送FIN请求,此时就处于半关闭状态,此时A可以接受B发送的数据,但不能像A发送数据

12345678#include int shutdown(int sockfd, int how); //sockfd:需要关闭的socket描述符 //how: //SHUT_RD(0):关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。 // 该套接字不再接受数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。 //SHUT_WR(1):关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发出写操作。 //SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以SHUT_WR。

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描述符。

123//在socket和bind之间插入下列代码 int opt = 1; setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

setsockopt和fcntl函数一样,用途很多,具体参考UNP第七章,这里只使用到他的端口复用功能

Select12345678910111213141516171819202122232425#include /* According to earlier standards */#include #include #include 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; /* seconds */ long tv_usec; /* microseconds */ }; void FD_CLR(int fd, fd_set *set); //把文件描述符集合里fd清0 int FD_ISSET(int fd, fd_set *set); //测试文件描述符集合里fd是否置1 void FD_SET(int fd, fd_set *set); //把文件描述符集合里fd位置1 void FD_ZERO(fd_set *set); //把文件描述符集合里所有位清0

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106//server.c#include #include #include #include "wrap.h"#define SERV_PORT 8888int main() { int i, j, n, maxi; int nready, client[FD_SETSIZE]; /* client数组方便遍历文件描述符 */ 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; /* reset表示select正在操作的监听集合,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); /* 起初listenfd 即为最大文件描述符 */ maxfd = listenfd; /* 用作client[]的下表,初值为第0个元素之前 */ maxi = -1; for (i = 0; i < FD_SETSIZE; i++) /* 用-1 初始化client[] */ client[i] = -1; /* 构造select监听集合 */ 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)); /* 寻找client中可用的位置,并将connfd加入 */ 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); } /* 向监听集合加入新的client */ FD_SET(connfd, &allset); if (connfd > maxfd) maxfd = connfd; /* maxi总是指向最后一个使用到的文件描述符 */ if (i > maxi) maxi = i; if (--nready == 0) continue; } for (i = 0; i <= maxi; i++) { /* 检测那个clients 有数据就绪*/ 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;}

Pollpoll和select的区别,select有上限1024,且除了重新编译内核之外没有办法修改,poll可以修改系统中的最大上限来更改。其次select中用于遍历文件描述符的数组,poll中变成了自带的,不需要再自定义了,其次select可以跨平台,poll针对linux

12345678910111213141516171819202122232425#include 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:等待指定毫秒数,如当前系统时间精度不够毫秒,向上取值

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112//server.c#include #include #include #include #include #include #include #include #include "wrap.h"#define MAXLINE 80#define SERV_PORT 8888#define OPEN_MAX 1024int main() { int i, j, maxi, listenfd, connfd, sockfd; /* 接收poll返回值,记录满足监听事件的fd个数 */ 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; /* 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); /* listenfd有读事件就绪 */ 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) { /* connection reset by client*/ if (errno == ECONNRESET) { /* 收到RST标志 */ 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;}

Epollpoll跟epoll的区别,epoll通过创建一颗红黑树来帮忙管理监听事件,比起我们自己处理监听数组,更方便快捷

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111//常规用法// #include // #include // #include // #include // #include // #include #include #include "wrap.h"#include #include #include #define MAXLINE 8192#define SERV_PORT 8888#define OPEN_MAX 1024int 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]; /* TEP: epoll_ctl参数 , ep[]: epoll_wait参数*/ 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); /* 创建epoll模型,实际上创建了一个文件描述符指向一颗红黑树,这棵红黑树就起到了之前select 和 poll里面数组的作用 */ efd = epoll_create(OPEN_MAX); if (efd == -1) perr_exit("epoll_Create error"); /* 对listenfd进行设置,监听读事件,data存放listenfd */ 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 (;;) { /* epoll_wait server阻塞监听事件,ep为struct epoll_event类型数组, open_max为数组容量, -1表示永久阻塞(就是阻塞时长那个参数) */ 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; /* 判断是否是listenfd, listenfd用来处理客户端的连接,其他的则是对应的read事件 */ 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,等待下一个监听事件

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172//一个例子,每次写10个数据,但每次只读5个,如果是水平触发,由于其特性回能够读完,而边沿触发//每次只能读5个,socket中的数据会积累的越来越多#include #include #include #include #include #define MAXLINE 10int main() { int efd, i; pid_t pid; int pfd[2]; char buf[MAXLINE], ch = 'a'; pipe(pfd); pid = fork(); //child if (pid == 0) { close(pfd[0]); while (1) { //aaaa\n for (i = 0; i < MAXLINE/2; i++) buf[i] = ch; buf[i-1] = '\n'; ch++; //bbbb\n for (;i < MAXLINE; i++) buf[i] = ch; buf[i-1] = '\n'; ch++; //aaaa\nbbbb\n write(pfd[1], buf, MAXLINE); sleep(3); } close(pfd[1]); //father } 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; /* ET边沿触发 */ // event.events = EPOLLIN; /* 默认LT水平触发 */ 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的情况下

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109//思路同上,socket实现//1.server.c#include #include #include #include #include #include #include #include #include #define MAXLINE 10#define SERV_PORT 7000int 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; /* ET 边沿触发 */ // event.events = EPOLLIN; /* 默认 LT 水平触发 */ 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;}//2.client.c#include #include #include #include #include #define MAXLINE 10#define SERV_PORT 7000int 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) { //aaaa\n for (i = 0; i < MAXLINE/2; i++) buf[i] = ch; buf[i-1] = '\n'; ch++; //bbbb\n for (;i < MAXLINE; i++) buf[i] = ch; buf[i-1] = '\n'; ch++; //aaaa\nbbbb\n write(sockfd, buf, sizeof(buf)); sleep(5); } close(sockfd); return 0;}

非阻塞I/O方式,设置socket非阻塞,并且使用边沿触发ET模式,通常为我们选择的方式,ET能够减少epoll_wait的调用次数,通过while轮询,也能解决ET模式无法将缓冲区读完的问题

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114//1.server.c#include #include #include #include #include #include #include #include #include #define MAXLINE 10 #define SERV_PORT 8888int 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; // event.events = EPOLLIN; 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)); /* 修改connfd为非阻塞 */ 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;}//2.client.c#include #include #include #include #include #define MAXLINE 10#define SERV_PORT 8888int 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) { //aaaa\n 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++; //aaaa\nbbbb\n write(sockfd, buf, sizeof(buf)); sleep(5); } close(sockfd); return 0;}

epoll反应堆模型

与普通模型相比,多了一步重新设置监听事件,这样的作用,可以判断是否能向客户端写事件,因为客户端不一定能写,同时用到了epoll_event的arg参数,自定义了一个回调函数,可以看看一些比较好的代码思路

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289//不是特别难,看的时候从main逐步向后看就能理解/* libevent 简易模型 *//* epoll 基于非阻塞I/O事件驱动 */#include #include #include #include #include #include #include #include #include #include #define MAX_EVENTS 1024 /* 监听上限数 */#define BUFLEN 4096#define SERV_PORT 8080void recvdata(int fd, int events, void* arg);void senddata(int fd, int events, void* arg);/* epoll_event data段结构 */struct my_events { int fd; // 监听的文件描述符 int events; // 对应的监听事件 void* arg; // 泛型参数 void (*call_back)(int fd, int events, void* arg); // 回调函数 int status; // 是否在监听 : 1.在红黑树上监听 0.不在 char buf[BUFLEN]; int len; long last_active; // 记录每次加入红黑树 g_efd 的时间值 用于超时判断,长时间不连接的话,将其从g_efd上删除};int g_efd; // 全局变量,保存epoll_create返回的文件描述符struct my_events g_events[MAX_EVENTS+1]; // 自定义结构类型数组 最后一位储存listenfd/* 将结构体my_events 初始化 */// 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->status = 0;// ev->last_active = time(NULL);// return;// // 错误代码 arg 没有初始化,导致后续回调的时候,无法写入到监听事件的buf中,段错误// }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; //memset(ev->buf, 0, sizeof(ev->buf)); //ev->len = 0; ev->last_active = time(NULL); //调用eventset函数的时间 return;}/* 想epoll监听的红黑树 添加一个 文件描述符 */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; // EPOLLIN 或 EPOLLOUT if (ev->status == 1) { // 已经在红黑树g_efd中,修改其属性 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++) // 从g_events中找出一个空闲元素 if (g_events[i].status == 0) // 类似于select中找一个-1元素 break; if (i == MAX_EVENTS) { // 跳出do while 不执行后续,实现了类似goto的功能 printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS); break; } int flag = 0; // 设置cfd为非阻塞 if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno)); break; } /* 给cfd设置一个结构体,回调函数设置为recvdata */ 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 ;}/* 从epoll监听的红黑树中删除一个文件描述符 */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); // 从gefd上将ev->fd摘除 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); // 读文件描述符,将数据读入my_events.buf 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); // 将回调函数设置为senddata printf("between set and add\n"); eventadd(g_efd, EPOLLOUT, ev); // 将fd重新加入,但是改为监听其写事件 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); // 设置为非阻塞 /* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */ eventset(&g_events[MAX_EVENTS], lfd, accpetconn, &g_events[MAX_EVENTS]); /* void eventadd(int efd, int events, struct myevent_s *ev) */ 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); // 创建epoll那棵树 if (g_efd <= 0) printf("create efd in %s err %s\n", __func__, strerror(errno)); initlistensocket(g_efd, port); // 初始化监听socket 就是listenfd struct epoll_event evetns[MAX_EVENTS + 1]; // 保存已满足就绪事件的文件描述符数组 printf("server running:port[%d]\n", port); int chekpos = 0, i; while (1) { /* 超时验证,每次测试100个连接,不测试listenfd,当客户端60秒没有和服务器通信,关闭客户端连接 */ 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"); /* 监听红黑树g_efd, 将满足的事件的文件描述符添加至events数组,1秒钟没有事件返回,返回0 */ 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。

线程池

把之前的一些芝士结合在一起了,不错

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428//1.threadpool.h#ifndef __THREADPOOL_H_#define __THREADPOOL_H_typedef struct threadpool_t threadpool_t;/** * @function threadpool_create * @descCreates a threadpool_t object. * @param thr_num thread num * @param max_thr_num max thread size * @param queue_max_size size of the queue. * @return a newly created thread pool or NULL */threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);/** * @function threadpool_add * @desc add a new task in the queue of a thread pool * @param pool Thread pool to which add the task. * @param function Pointer to the function that will perform the task. * @param argument Argument to be passed to the function. * @return 0 if all goes well,else -1 */int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg);/** * @function threadpool_destroy * @desc Stops and destroys a thread pool. * @param pool Thread pool to destroy. * @return 0 if destory success else -1 */int threadpool_destroy(threadpool_t *pool);/** * @desc get the thread num * @pool pool threadpool * @return # of the thread */int threadpool_all_threadnum(threadpool_t *pool);/** * desc get the busy thread num * @param pool threadpool * return # of the busy thread */int threadpool_busy_threadnum(threadpool_t *pool);#endif///////////////////////////////////////////////////////////////////////////////////2.threadpool.c#include #include #include #include #include #include #include #include #include "threadpool.h"#define DEFAULT_TIME 10 /*10s检测一次*/#define MIN_WAIT_TASK_NUM 10 /*如果queue_size > MIN_WAIT_TASK_NUM 添加新线程到线程池*/#define DEFAULT_THREAD_VARY 10 /*每次创建或销毁线程的个数*/#define true 1#define false 0typedef struct { void*(*function)(void*); /*函数指针,回调函数*/ void* arg; /*回调函数的参数*/} threadpool_task_t; /*各子线程任务结构体*//*描述线程池相关信息*/struct threadpool_t { pthread_mutex_t lock; /*锁住本结构体*/ pthread_mutex_t thread_counter; /*锁住忙状态个数 -- busy_thr_num*/ pthread_cond_t queue_not_full; /*当任务队列满时,添加任务的线程阻塞,等待此条件变量*/ pthread_cond_t queue_not_empty; /*任务队列不为空时,通知等待的线程*/ threadpool_task_t* task_queue; /*任务队列*/ pthread_t* threads; /*存放线程池中每个线程的tid数组*/ pthread_t adjust_tid; /*管理线程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; /*task_queue队头下标*/ int queue_rear; /*task_queue队尾下表*/ int queue_size; /*task_queue队中实际任务数*/ int queue_max_size; /*task_queue队列可容纳任务数上限*/ int shutdown; /*标志位,线程池使用状态*/};/** * @function void *threadpool_thread(void *threadpool) * @desc the worker thread * @param threadpool the pool which own the thread */void* threadpool_thread(void* threadpool);/** * @function void *adjust_thread(void *threadpool); * @desc manager thread * @param threadpool the threadpool */void* adjust_thread(void* threadpool);/** * check a thread is alive */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; /*有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; } /*启动min_thr_num个work_thread*/ 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)); /* ==为真,队列已满,调wait阻塞*/ 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)); } /*清空 工作线程 调用的回调函数 的参数arg*/ 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)); /*queue_size == 0 说明没有任务,调wait阻塞在条件变量上,若有任务,跳过该while*/ 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)); /*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/ 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); } } } /*如果指定了true,要关闭线程池里每一个线程,自行退出处理*/ 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)); /*忙线程+1*/ 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)); /* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/ if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) { pthread_mutex_lock(&(pool->lock)); int add = 0; /*一次增加 DEFAULT_THREAD 个线程*/ 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)); } /* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 存活的线程数 大于 最小线程数时*/ if ((busy_thr_num * 2) < live_thr_num && live_thr_num > pool->min_thr_num) { /* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */ pthread_mutex_lock(&(pool->lock)); pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */ 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); //发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);/*创建线程池,池里最小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为了防止丢包,可以通过改变缓冲区大小,

1234#include int setsockopt(int sockfd, int level, int optname, const void* optval, socklen_t optlen);int n = 220x1024 /*推荐值,通过经验得出的, 可以根据自己的需求更改*/setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &N, sizeof(n));

UDP C/S模型和TCP的区别,不需要accpet和客户端连接了,直接读就行了,所以也可以引出另一个特点,UDP通信自带多线程/多进程了,不需要向TCP那样必须使用多进程/多线程才能完成与多个客户端的通信

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687//1.server.c#include #include #include #include #include #define SERV_PORT 8000int 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;}//2.client.c#include #include #include #include #include #define SERV_PORT 8000int 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的选项(允许发送广播数据报)

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788//1.server.c#include #include #include #include #include #include #include #define SERVER_PORT 8000#define MAXLINE 1500#define BROADCAST_IP "172.23.175.255"#define CLIENT_PORT 9000int main() { int sockfd; struct sockaddr_in serv_addr, clie_addr; char buf[MAXLINE]; /*构造UDP通信的套接字*/ 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)); /*构造 client 地址 IP+端口 172.23.175.255+9000*/ 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;}//2.client.c#include #include #include #include #include #define SERVER_PORT 8000#define MAXLINE 4096#define CLIENT_PORT 9000int 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命令查看网卡编号

123456789101112131415161718192021222324252627282930313233//1.例子itcast$ ip ad1: lo: 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 forever2: eth0: mtu 1500 qdisc pfifo_fast state DOWN group default qlen 1000 link/ether 00:0c:29:0a:c4:f4 brd ff:ff:ff:ff:ff:ff inet6 fe80::20c:29ff:fe0a:c4f4/64 scope link valid_lft forever preferred_lft forever//2.实际1: lo: 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 forever2: bond0: mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 1a:fc:2a:2e:5a:28 brd ff:ff:ff:ff:ff:ff3: dummy0: mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether ca:9d:9f:9b:96:7f brd ff:ff:ff:ff:ff:ff4: eth0: mtu 1500 qdisc mq state UP group default qlen 1000 link/ether 00:15:5d:6e:db:0c 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:5dff:fe6e:db0c/64 scope link valid_lft forever preferred_lft forever5: tunl0@NONE: mtu 1480 qdisc noop state DOWN group default qlen 1000 link/ipip 0.0.0.0 brd 0.0.0.06: sit0@NONE: mtu 1480 qdisc noop state DOWN group default qlen 1000 link/sit 0.0.0.0 brd 0.0.0.0

代码部分和广播有点像,但有一点区别,首先同样也得开发组播权限通过setsockopt ,其次服务器端和客户端有一定区别,服务器端是向某组播地址广播,客户端是加入某组播地址

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394//1.server.c#include #include #include #include #include #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); // 本地任意IP 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;}//2.client.c#include #include #include #include #include #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小结

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394//图1.5 client#include #include #include #include #include #include #include #include #define MAXLINE 4096#define LISTENQ 1024intmain(int argc, char **argv){ int sockfd, n, counter = 0; char recvline[MAXLINE + 1]; struct sockaddr_in servaddr; if (argc != 2) perror("usage: a.out "); 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); /* daytime server */ 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; /* null terminate */ if (fputs(recvline, stdout) == EOF) perror("fputs error"); } if (n < 0) perror("read error"); printf("counter=%d\n", counter); exit(0);}//图1.9 server#include #include #include #include #include #include #include #include #include #define MAXLINE 4096#define LISTENQ 1024intmain(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); /* daytime server */ 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)); // write(connfd, buff, strlen(buff)); for (i=0; i

尽管服务器端,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套接口地址结构

1234567891011//它以sockaddr_in命名,定义在头文件struct in_addr { in_addr_t s_addr; /* 32-bit IPV4 address */};struct sockaddr_in { uint8_t sin_len; /* length of structure (16) */ sa_family_t sin_family; /* AF_INET */ in_port_t sin_port; /* 16-bit TCP or UDP port number */ struct in_addr sin_addr; /* 32-bit IPV4 address */ char sin_zero[8]; /* unused */};

sin_addr成员由于历史原因是一个结构,早期定义为不同结构的联合。

POSIX规范要求的数据类型

通用套接口地址结构

123456//定义在struct sockaddr { uint8_t sa_len; sa_family_t sa_family; /* address family: AF_XXX value */ char sa_data[14]; /* protocol-specific address */};

套接口函数被定义为采用指向通用套接口地址结构的指针,如bind函数的原型

1int bind(int, struct sockaddr*, socklen_t);

其实可以更好的采用void来实现,最终为sockaddr同样因为历史原因,套接口函数的定义早于void*

IPV6套接口地址结构

12345678910111213//定义在struct in6_addr { uint8_t s6_addr[16]; /* 128-bit IPV6 address */};#define SIN6_LENstruct sockaddr_int6 { uint8_t sin6_len; /* length of thie struct (28) */ uint8_t sin6_family; /* AF_INET6 */ sa_family_t sin6_port; /* transport layer port */ uint32_t sin6_flowinfo; /* flow information undefined */ struct in6_addr sin6_addr; /* IPV6 address */ uint32_t sin6_scope_id; /* set of interfaces for a scope */};

具体成员说面,见书

新的通用套接口地址结构

1234567//定义在struct sockaddr_storage { uint8_t ss_len; /* length of this struct */ sa_family_t ss_family; /* address family: AF_XXX value */};

与原先的通用结构的差别:

如果系统支持的任何套接口地址结构又对齐需求,那么sockaddr_storage能够满足最苛刻的对齐要求

sockaddr_storage足够大,能够容纳系统支持的任何套接口地址结构

套接口地址结构的比较

看图即可

3.3 值结果参数简单来说,函数的定义传进时作为值,给函数提供条件,结束时将结果返回给这个参数,所以这个参数可以是指针类型,能够将函数内改动的结果带出来

1234567//egstruct sockaddr_un cil; /* unix domain */socklen_t len;len = sizeof(cil);getpeername(unixfd, (sturct sockaddr*)&cil, &len);//后参数类型: 套接口地址结构指针和表示结构大小的整数的指针

3.4 字节排序函数(htons,htonl,ntohs,ntols)12345678910111213141516171819//检验主机字节序的函数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)); }

由于这两种字节序没有标准,且都有系统使用,而网络协议在处理这些多字节整数时,使用大端字节序,所以需要一系列转换函数

1234567#include 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,其他用到的时候再看

1234567891011#include 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); //返回:0--相等, 非0--不相等#include 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); //返回:0--相等, >0 or <0 -- 不相等

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都能处理

1234567#include int inet_aton(const char* strptr, struct in_addr* addrptr); //返回:1--串有效, 0--串有错in_addr_t inet_addr(const char* strptr); //若成功,返回32位二进制的网络字节序地址;若有错,返回INADDR_NONEchar* inet_ntoa(struct in_addr inaddr); //返回指向点分十进制数串的指针

inet_addr可能出现的一个问题,出错时返回的INADDR_NONE 其值为一个32位均为1的值,也就意味着255.255.255.255不能由该函数处理,它的二进制值与INADDR_NONE一样

3.7 inet_pton和inet_ntop函数12345#include int inet_pton(int family, const char* strptr, void* addrptr); //返回: 1 -- 成功, 0 -- 输入的不是有效的表达式, -1 -- 出错const char* inet_ntop(int family, const void* addrptr, char* strptr, size_t len); //返回: 指向结果的指针 -- 成功, NULL -- 出错

两函数均以参数作为结果的返回值,pton以addrptr为转换后的结果,ntop以strptr为储存的结果,所以strptr不能时空指针,len的大小,为有助于规定这个大小,在****中有如下定义

12#define INET_ADDRSTRLEN 16 /* for IPV4 dotted-decimal */#define INET6_ADDRSTRLEN 46 /* for IPV6 hex string */

所有转换函数图示一览

12345678910//eginet_pton(AF_INET, cp, &foo.sin_addr);//替代foo.sin.addr_s_addr = inet_addr(cp);//egchar 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及其他一系列函数,则将这种情况避免,实现协议无关的套接字函数,如下为其一的定义

12345678910111213141516171819202122#include "unp.h"char* sock_ntop(const struct sockaddr* sockaddr, socklen_t addrlen); //简略实现,仅考虑AF_INET的情况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由于某些影响没有执行完的情况,这些函数遇到这种情况后,会再次调用,也就是不需要使用者操心这些没有执行完的情况了

12345//定义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出错

123456789101112131415161718192021//readn实现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; /* and call read() again */ else return -1; } else if (nread == 0) break; nleft -= nread; ptr += nread; } return (n-nleft);}

123456789101112131415161718192021//writen实现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; /* and call write() again */ else return -1; } nleft -= nwritten; ptr += nwritten; } return n;}

12345678910111213//readlinessize_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函数123#include int socket(int family, int type, int protocol); //非负描述字 -- 成功, -1 -- 出错

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服务器的连接

123#include int connect(int sockfd, const struct sockaddr* servaddr, socklen_t addrlen); //返回0 -- 成功 , -1 -- 出错

sockfd是由socket函数返回的套接口描述字,第二第三参数分别是一个指向套接口地址的结构的指针和该结构的大小。客户在调用函数connect前不必非得调用bind函数,需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。如果是TCP套接口,调用connect函数将激发TCP的三路握手过程(2.6),并且仅在连接建立成功或出错时菜返回

4.4 bind函数bind函数把一个本地协议地址赋予一个套接口,对于网际协议,协议地址是32位的IPV4地址或128位的IPV6地址与16位的TCP或UDP端口号的组合

123#include int bind(int sockfd, const struct sockaddr* myaddr, socklen_t addrlen); //0 -- 成功 , -1 -- 出错

指定端口号为0,或指定地址值为INADDR_ANY则由内核选择端口号或指定IP地址

123456//to IPV4struct sockaddr_in servaddr;servaddr.sin_addr.s_addr = htonl(INADDR_ANY);//to IPV6struct sockaddr_in6 serv;serv.sin6_addr = in6addr_any;

如果让内核来选择临时值,由于第二参数有const修饰,它无法返回所选之值。为了得到内核所选择的这个临时端口值,必须调用函数 getsockname 来返回协议地址

4.5 listen函数listen函数仅由TCP服务器调用,它做两件事

当socket函数创建一个套接口时,它被假设为一个主动套接口,也就是说,他是一个将调用connect发起连接的客户套接口。listen函数把一个未连接的套接口,转换成一个被动套接口。由TCP状态转换图,调用listen导致套接口从CLOSED状态转换到LISTEN状态

该函数的第二个参数规定了内核应该为相应套接口排队的最大连接个数

123#include int listen(int sockfd, int backlog); //0 -- 成功 , -1 -- 出错

4.6 accept函数accpet函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠

123#include int accept(int sockfd, struct sockaddr* cliaddr, soclen_t* addrlen); //非负描述字 -- 成功 , -1 -- 出错

参数cliaddr,addrlen用来返回对端进程的协议地址,如果不需要这两个参数,可以置为NULL

123456789101112//用例...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函数123#include pid_t fork(void); //子进程中返回0 , 父进程中返回子进程ID , -1 -- 出错

详细的参考APUE note

进程在调用exec之前打开着的描述字通常跨exec继续保持打开,但可以通过fcntl设置FD_CLOEXEC描述字标志禁止掉

4.8 并发服务器1234567891011121314151617181920//简单的轮廓pid_t pid;int listenfd, connfd;listenfd = Socket(...); /*fill in sockaddr_in {} with server's well-known port */Bind(listenfd, ...);Listen(listenfd, LISTENQ);for (;;) { connfd = Accept(listenfd, ...); /* probably blocks */ if ((pid = Fork()) == 0) { Close(listenfd); /* child close listening socket */ doit(connfd); /* process the requet */ Close(connfd); /*done with this client */ exit(0); /* child terminates */ } Close(connfd); /* parent closes connected socket */}

为什么close两次,因为每个文件或套接口都有一次引用计数,而fork返回后,描述字在父子进程间共享,因此着两个套接口的计数为2

4.9 close函数Unix通常的close函数也用来关闭套接口,并终止TCP连接

123#include int close(int sockfd); //0 -- 成功 , -1 -- 出错

close实际上是引用计数减一并不会发送FIN,只有在计数为0后,才能达到我们想要的效果,如果想在某个TCP连接发送一个FIN,可以改用shutdown函数(6.6)

4.10 getsockname和getpeername函数这两个函数返回与某个套接口关联的本体协议地址,或者返回与某个套接口关联的远地协议地址

1234#include int getsockname(int sockfd, struct sockaddr* localaddr, socklen_t* addrlen);int getpeername(int sockfd, struct sockaddr* peeraddr, socklen_t* addrlen); //0 -- 成功 , -1 -- 出错

12345678910//示例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回射服务器12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#include #include #include #include #include #include #include #define SERV_IP "10.0.0.14"#define SERV_PORT 7777#define MAXLINE 1024void 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 from IP: %s , PORT: %d\n", // inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,cli_ip, sizeof(cli_ip)), // ntohs(cliaddr.sin_port)); printf("connect success form,client IP:**, client port:%d\n", // inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,clie_IP, sizeof(clie_IP)), 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回射客户端123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354#include #include #include #include #include #include #include #include #define MAXLINE 1024#define SERV_IP "10.0.0.14"#define SERV_PORT 7777void 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); // read(sockfd, buf, sizeof(buf)); // int n = write(sockfd, buf, sizeof(buf)); // fputs(buf, stdout); 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 清理僵尸进程

123456789void sig_chld(int signo) { pid_t pid; int stat; pid = wait(&stat); printf("child %d terminated\n", pid); return; //在信号处理函数中显式的给出return语句 //当某个系统调用被我们编写的信号处理函数中断时,可以得到被哪个信号处理函数终端的}

5.10 wait和waitpid函数1234#include pid_t wait(int* statloc);pid_t waitpid(pid_t pid, int* statloc, int options); //返回: 进程ID -- 成功 , -1 -- 出错

详细的区别见AUPE

12345678910//上述处理函数用wait,在并发多个子进程的时候,由于信号只会处理一次,后续无论排队多少个信号,仍然只会处理一次,所以需要循环waitvoid 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在有尚未终止子进程在运行时不要阻塞

12345678910111213141516171819202122232425262728293031323334353637int 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); /* must call waitpid */ for (;;) { clilen = sizeof(cliaddr); if ((connfd = accept(listenfd, (strucr sockaddr*)&cliaddr, &clilen)) < 0) { if (errno == EINTR) continue; /* back to for */ else err_sys("accept error"); } if ((childpid = Fork()) == 0) { /* child process */ Close(listenfd); /* close listening socket */ str_echo(connfd); /* process the request */ } close(connfd); /* parent close connected socket */ }}

本节示范的三个目的:

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信号。该信号缺省行为是终止进程

123456789101112//掩饰SIGPIPE信号会发生什么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函数12345678910#include #include int select(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout); //返回: 就绪描述字的正数目, 0 -- 超时, -1 -- 出错struct timeval { long tv_sec; /* seconds */ long tv_usec; /* mircroseconds */};

timeout有三种可能:

设置为空指针,永远等下去

等待一定固定时间,不超过该参数设置的值

不等待,检查描述字后立即返回,轮询。 参数设置为0

中间的三个参数分别为:我们要让内核测试的读,写和异常条件的描述字。目前支持异常条件只有两个:

某个套接口的带外数据的到达。24章

某个已设为分组方式的伪终端存在可从其主端读取的控制状态信息。

123456789101112//fd_set的实现细节不深究,可能是数组,第一个元素对应描述字0-31,一次类推void FD_ZERO(fd_set* fdset); /* clear all bits in fdset */void FD_SET(int fd, fd_set* fdset); /* turn on the bit for fd in fdset*/void FD_CLR(int fd, fd_set* fdset); /* turn off the bit for fd in fdset*/int FD_ISSET(int fd, fd_set* fdset); /* is the bit for fd on in fdset*///egfd_set rset;FD_ZERO(&rset);FD_SET(1, &rest);FD_SET(4, &rset);FD_SET(5, &rest);

6.4 str_cli函数

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758//书中的例子#include "unp.h"voidstr_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)) { /* socket is readable */ if (Readline(sockfd, recvline, MAXLINE) == 0) err_quit("str_cli: server terminated prematurely"); Fputs(recvline, stdout); } if (FD_ISSET(fileno(fp), &rset)) { /* input is readable */ if (Fgets(sendline, MAXLINE, fp) == NULL) return; /* all done */ Writen(sockfd, sendline, strlen(sendline)); } }}//myvoid 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连接是双全工的,有时候我们只需要关闭一般

12345678#include int shutdown(int sockfd, int howto); //返回: 0 -- 成功 , -1 -- 出错howto的参数值: SHUT_RD:关闭连接读的这一半 SHUT_WR:关闭连接写的这一半 -- 对于TCP套接口,这称为半关闭(half-close) SHUT_RDWR:读写都关闭 -- 这与调用两次shutdown分别SHUT_RD 和 SHUT_WR等效

6.7 str_cli函数(再修订版)12345678910111213141516171819202122232425262728293031323334353637void 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 单线程来重写之前的回射服务器

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100#include #include #include #include #include #include #include #include #include #define SERV_IP "10.0.0.14"#define SERV_PORT 7777#define MAXLINE 1024int 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", // inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,clie_IP, sizeof(clie_IP)), 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函数1234567891011#include #include #include int pselect(int maxfdp1, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timesepc* timeout, const sigset_t* sigmask); //返回: 就绪的描述字的个数 0 -- 超时 , -1 -- 出错struct timespec { time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */}

与select的相比两个变化:

使用timespec结构,精度由微妙到纳秒了

增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再由测试这些当前被禁止的信号的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码

简单来说,在pselect期间,以一新的信号屏蔽集替换当前的,pselect返回之后替换回来

6.10 poll函数poll函数提供的功能与select类似,但在处理流设备时,它能提供额外的信息

1234567891011#include int poll(struct pollfd* fdarray, unsigned long nfds, int timeout); //返回: 就绪描述字的个数, 0 -- 超时, -1 -- 出错struct pollfd { int fd; short events; short revents;}//fd:指定描述字//events:指定要测试的条件//revents:返回该描述字的状态

结构数组中元素的个数由nfds参数指定。timeout参数指定poll函数返回前等待多长时间。

如果不关心某个特定描述字,可以把它对应的pollf的fd成员设置成一个负值

6.11 TCP回射服务器程序(再修订版)123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899#include #include #include #include #include #include #include #include #define SERV_IP "10.0.0.14"#define SERV_PORT 7777#define MAXLINE 1024#define INFTIM -1#define OPEN_MAX 1024int 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", // inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr,clie_IP, sizeof(clie_IP)), 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函数123456#include 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); //据返回: 读写字节数 -- 成功, -1 -- 出错

**flags:**在14章讨论,目前总把flags设为0

**to:**指向一个含有数据报接收者的协议地址的套接口地址结构,其大小由addrlen参数指定

**from:**类似于accept的后两个参数,告诉我们是谁发送了数据报(udp情况下)或是谁发起了连接(tcp情况下)

写一个长度为0的数据报是可行的。在UDP情况下,这导致一个只包含一个IP头部(IPV4 20字节,IPV6 40字节)和一个8字节的UDP头部而没有数据的IP数据报。

8.3 UDP回射服务器程序12345678910111213141516#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函数1234567891011void 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回射客户程序1234567891011121314151617181920212223242526#include #include #include #include #include #include #define SERV_IP "10.0.0.14"#define SERV_PORT 7777#define MAXLINE 1024int 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函数12345678910111213void 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 验证收到的响应1234567891011121314151617181920212223void 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函数(修订)123456789101112131415void 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地址而选定

123456789101112131415161718192021222324#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 "); 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回射服务器程序12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091//大致思路,没编译#include #include #include #include #include #include #include #include #include #include #define MAXLINE 1024#define SERV_IP "10.0.0.14"#define SERV_PORT 7777int 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); //tcp 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); //udp 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)); //设置信号捕捉函数,wait回收子进程,避免产生僵尸进程 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地址。

123456789101112#include struct hostent* gethostbyname(const char* hostname); //返回: 非空指针 -- 成功, 空指针 -- 出错,同时设置h_errno//hostent结构struct hostent { char* h_name; /* official (canonical) name of host */ char** h_aliases; /* pointer to array of pointers to alias names */ int h_addrtype; /* host addrss type: AF_INET */ int h_length; /* length of aadrss: 4 */ char** h_addr_list; /* ptr to array of ptrs with IPV4 addrs */}

按照DNS的说法,getbyname执行的是对A记录的查询。他只能返回IPV4地址。

下图所查询的主机名有2个别名和3个IPV4地址。

gethostbyname发生错误时,它不设置errno变量,而是将全局整数变量h_errno设置为在中定义的下列常值之一:

HOST_NOT_FOUND

TRY_AGAIN

NO_RECOVERY

NO_DATA(等同于NO_ADDRESS)

多数解析器提供名为hstrerror的函数,以某个h_errno值作为唯一的参数

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960//eg#include #include #include #include #include #include #include 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: 075097c3da50476181f422ef916e1460 Boot ID: 149585e4f7384a71833fa6a9f2b6293d Virtualization: kvm Operating System: Ubuntu 20.04 LTS Kernel: Linux 5.4.0-126-generic Architecture: x86-64ubuntu@VM-0-14-ubuntu:~/learning/2023/UnpStudy/chapter11$ ./hostent VM-0-14-ubuntuofficial hostnamelocalhost.localdomain alias: VM-0-14-ubuntu address: 127.0.1.1

正式主机名就是FQDN

11.4 gethostbyaddr函数gethostbyaddr函数试图由一个二进制的IP地址找到响应的主机名,与gethostbyname的行为刚好相反

123#include struct hostent* gethostbyaddr(const char * addr, socklen_t len, int family); //返回: 非空指针 -- 成功 , 空指针 -- 出错 , 同时设置h_errno

adrr参数实际上不是char* 类型, 而是一个指向存放ipv4地址的某个in_addr结构的指针,len参数是这个结构的大小,对于IPV4地址为4,family参数为AF_INET

按照DNS的说法,gethostbyaddr在in_addr.arpa域中向一个名字服务器查询PTR记录

11.5 getservbyname和getservbyport函数如果我们在程序代码中通过其名字而不是其端口号来指代一个服务,而且从名字到端口号的映射关系保存在一个文件中(通常是/etc/services)。getservbyname函数用于根据给定名字查找相应服务

1234567891011#include struct servent* getservbyname(const char * servname, const char * protoname); //返回: 非空指针 -- 成功 , 空指针 -- 出错//servent结构struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number, network-byte order */ char *s_proto; /* protocol to ues */};

服务器参数servname必须指定。如果同时指定了协议(protoname参数为非空指针),那么指定服务必须有匹配的协议。

123456//本函数的典型调用如下struct servent *sptr;sptr = getservbyname("domain", "udp"); /* DNS using UDP */sptr = getservbyname("ftp", "tcp"); /* FTP using TCP */sptr = getservbyname("ftp", NULL); /* FTP using TCP */sptr = getservbyname("ftp", "udp"); /* this call with fail */

既然FTP仅支持TCP,第2个调用和第3个调用等效,第4个调用则会失败。以下是/etc/services文件中的典型文本行:

getservbyport用于根据端口号和可选协议查找相应服务

123#include struct servent* getservbyport(int port, const char * protoname); //返回: 非空指针 -- 成功 , 空指针 -- 出错

port的参数值必须为网络字节序,典型调用如下:

12345struct servent *sptr;sptr = getservbyport(htons(53), "udp"); /* DNS using UDP */sptr = getservbyport(htons(21), "tcp"); /* FTP using TCP */sptr = getservbyport(htons(21), NULL); /* FTP using TCP */sptr = getservbyport(htons(21), "udp"); /* this call will fail */

既然UDP上没有服务使用端口21, 最后一个调用将失败

有些端口号在TCP上用于一种服务,在UDP上却用于完全不同的另一种服务,例如

例子:使用gethostbyname和getservbyname

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859#include "unp.h"int main(int argc, char **argv) { int sockfd, n; char recvline[MAXLINE+1]; struct sockaddr_in servaddr; struct in_addr **pptr; /* in_addr 结构体表示32位IPV4地址 */ struct in_addr *inetaddrp[2]; struct in_addr inetaddr; struct hostent *hp; struct servent *sp; if (argc != 3) err_quit("usage: daytimetcpcli1 "); //查找地址,若没有找到,则采用inet_aton确定其参数是否以是ASCII格式的地址 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; /* success */ 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结构的链表。

12345678910111213141516#include int getaadrinfo(const char * hostname, const char * service, const struct addrinfo * hints, struct addrinfo **result); //返回: 0 -- 成功 , 非0 -- 出错//addrinfo结构定义在struct addrinfo { int ai_flags; /* AI_PASSIVE, AI_CANONNAME */ int ai_family; /* AF_XXX */ int ai_socktype; /* SOCK_XXX */ int ai_protocol; /* 0 or IPPROTO_xxx for IPV4 and IPV6 */ socklen_t ai_addrlen; /* length of ai_addr */ char *ai_canonname; /* ptr to canonical name for host */ struct sockaddr *ai_addr; /* ptr to socket address structure */ struct addrinfo *ai_next; /* ptr to next structure in linked list */};

前两个参数没啥好说的,第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成员指向所查找主机的规范名字。

123456//图11.5 给出了执行下列程序片段返回的信息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差不多

123#include const char * gai_strerror(int error); //返回:指向错误描述信息字符串的指针

11.8 freeaddrinfo 函数用来清除getaddrinfo返回的addrinfo链表,这个链表的所有结构以及由它们指向的任何动态存储空间(譬如套接口地址结构和规范主机名)都被释放掉

12#include void freeaddrinfo(struct addrinfo * ai);

11.9 getaddrinfo函数: IPV6POSIX规范定义了getaddrinfo函数以及该函数为IPV4或IPV6返回的信息。在以图11.8汇总这些返回值之前,我们注意以下几点:

11.10 getaadrinfo 函数:例子在附加hitns期望时的,返回结果

11.11 host_serv 函数一个getaddrinfo的接口函数,增加我们刚兴趣的两个成员,family和socktype作为参数,其他实现在函数内部进行

1234567891011121314151617181920#include "unp.h"struct addrinfo * host_serv(const char * hostname, const char * service, int family, int socktype); //返回: 指向addrinfo结构的指针 -- 成功 , NULL -- 出错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; /* always return canonical name */ hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6 etc. */ hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ if((n = getaddrinfo(host, serv, &hints, &res)) != 0) return NULL; return res; /* return pointer to first on linked list */}

11.12 tcp_connect 函数也是一个自定义函数。处理TCP客户和服务器大多数情形的两个函数。第一个即tcp_connect执行客户的通常步骤:创建一个TCP套接口并连接到一个服务器

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667#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; /* ignore this one */ if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) break; close(sockfd); /* success */ } while((res = res->ai_next) != NULL); if (res == NULL) /* errno set from final connect() */ 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));}//eg 将图1.5的时间获取客户程序改用tcp_connect重新编写#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 "); 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套接口,给它捆绑服务器众所周知端口,并允许接受外来的连接请求。

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105#include #include #include #include #include #include #include #include #include #include #include #include #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 \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); } //验证addrinfo获取的信息 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"); //在AF_INET的情况下,sockaddr等同于sockaadr_in,两者内存布局一样 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套接口

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263#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 "); 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套接口

1234567891011121314151617181920212223242526272829303132333435#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

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071#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 #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 [ ] "); 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的互补函数:它以一个套接口地址为参数,返回描述其中的主机的一个字符串和描述其中服务的另一个字符串。本函数以协议无关的方式提供信息

12345#include int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags); //0 -- 成功 , 非0 -- 出错

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)。考虑如下例子

1234567891011121314151617main (){ struct hostent *hptr; ... signal(SIGALRM, sig_alrm); ... hptr = gethostbyname(...); ...}void sig_alrm(int signo){ struct hostent *hptr; ... hptr = gethostbyname(...); ...}

如果主控制流被暂停时正处于执行gethostbyname期间(比如已经填写好host变量准备返回),由于随后信号处理函数再一次调用gethostbyname,该host变量被重用,原先主控制流计算出的值被重写成了信号处理函数调用计算出的值。

同理ernno变量,存在同样的问题,比如如下例子:

1234if (close(fd) < 0) { fprintf(stderr, "close error, errno = %d\n", errno); exit(1);}

如果输出errno的值之前,一个信号处理函数被执行,同时执行另一个系统调用发生错误,则由close设置的errno值被覆写

一种解决方法

在信号处理函数,提前保存,返回之前在恢复

1234567void 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 ( ? )

12345678#include 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); // 非空指针 -- 成功 NULL -- 出错

每个函数都需要4个额外的参数。result参数指向调用者分配并由被调用函数填写的hostent结构

buf参数指向由调用者分配且大小为buflen的缓冲区。如果出错,错误通过h_errnop返回

11.20 作废的IPV6地址解析函数在开发IPV6期间,用于查找IPV6地址的API经历了若干次反复。最终在RFC 3493中被简单替换成getaddrinfo和getnameinfo。

RES_UES_INET6常值

gethostbyname2函数

123#include struct hostent * gethostbyname2(const char * name, int af); //非空指针 -- 成功 , NULL -- 出错,同时设置h_errno

当af参数为AF_INET时,gethostbyname2的行为与gethostbyname一样,即查找并返回IPV4地址。当af参数为AF_INET6时,gethostbyname2只查找AAAA记录并返回IPV6地址

getipnodebyname函数

1234#include struct hostent * getipnodebyname(const char * name, int af, int flags, int *error_num); //非空指针 -- 成功 , NULL -- 出错,同时设置error_num

af和flags参数映射到getaadrinfo的hints.ai_family和hints.ai_flags参数。

为了线程安全,返回值是动态的,因而必须使用freehostent函数释放

12#include 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设置超时

1234567891011121314151617181920212223242526272829303132#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; //设置信号处理函数,并保存原先的handler用于恢复 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); //如果是因为系统调用被信号打断,将errno设置为ETIMEDOUT if (errno == EINTR) errno = ETIMEDOUT; } alarm(0); /* turn off the alarm */ Signal(SIGALRM, sigfunc); /* restore previous signal handler */ return (n);}static void conncet_alarm(int signo){ return;}

两点问题:

1.由于connect的超时通常为75s,此技术可以指定比75s小的值,但指定大于75的值,conncet仍将在75s超时

2.存在一些系统调用返回EINTR时,会重新执行同一个系统调用

使用SIGALRM为recvfrom设置超时

1234567891011121314151617181920212223242526272829303132#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]; //设置handler 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设置超时

12345678910111213141516171819202122232425262728293031323334353637#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)); /* >0 if descriptor is readable */}//调用readable_timeo的dg_cli函数#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仅适用与写操作

12345678910111213141516171819202122232425262728#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) { //如果I/O操作超时,将返回一个EWOULDBLOCK错误 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函数,不过需要一个额外的参数

1234#include 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); //返回:读入或写出字节数 -- 成功 , -1 -- 出错

flags的参数要么为0,要么为下值

flags参数设计成值传递,而不是值-结果参数,因此只能从进程向内核,而不能把内核的标志传回来。后续提出了这个需求,但没有改变这个函数的参数,将这个需求加入了recvmsg和sendmsg所用的msghdr结构。该结构新增了一个整数msg_flags成员。

14.4 readv和writev函数readv和writev允许单个系统调用读入到或写出自一个或多个缓冲区。分散读和集中写,将读操作的数据分散到多个应用缓冲区,将多个应用缓冲区的数据提供给单个写操作

12345678910#include ssize_t readv(int filedes, const struct iovec *iov, int iovcnt);ssizr_t writev(int filedes, const struct iovec *iov, int iovcnt); //返回: 读入或写出字节数 -- 成功 , -1 -- 出错//struct iovecstruct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */};

第2个参数指向某个iovec结构数组的一个指针,iovcnt应该是这个数组元素的数量

readv和writev这两个函数可用于任何描述字,而不仅限于套接口。另外writev是一个原子操作

14.5 recvmsg和sendmsg函数123456789101112131415#include ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);ssize_t sendmsg(int sockfd, struct msghdr *msg, int flags); //返回: 读入或写出字节数 -- 成功 , -1 -- 出错//msghdr结构struct msghdr { void *msg_name; /* protocol address */ socklen_t msg_namelen; /* size of protocol address */ struct iovec *msg_iov; /* scatter/gather array */ int msg_iovlen; /* elements in msg_iov */ void *msg_control; /* ancillary data (cmsghdr struct) */ socklen_t msg_controllen; /*length of ancillary data */ int msg_flags; /* flags returned by recvmsg */}

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这两个成员发送和接收。

辅助数据的各种用途见下图:

1234567#include struct cmsghdr { socklen_t cmsg_len; /* length in bytes, including this structure */ int cmsg_level; /* roiginating protocol */ int cmsg_type; /* protocol-specific type */ /* followed by unsigned char cmsg_data[] */};

由于recvmsg返回的辅助数据可含有任意数目的辅助数据对象,为了对应用程序屏蔽可能出现的填充文字,头文件种定义了以下5个宏,以简化对辅助数据的对处理

123456789101112#include #include struct cmsghdr * CMSG_FIRSTHDR(struct msghdr *mhdrptr); //返回:指向第一个cmsghdr结构的指针,无辅助数据时为NULLstruct cmsghdr * CMSG_NXTHDR(struct msghdr *mhdrptr, struct cmsghdr *cmsgptr); //返回:指向下一个cmsghdr结构的指针,不再有辅助数据对象时为NULLunsigned char * CMSG_DATA(struct cmsghdr *cmsgptr); //返回:指向与cmsghdr结构关联的数据的第一个字节的指针unsigned int CMSG_LEN(unsigned int lenght); //返回:给定数据量下存放到cmsg_len中的值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流:一个用于读,一个用于写

1234567891011121314151617181920212223//标准I/O重写图5.3的TCP回射服务器程序#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.96hello, 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 接口

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061struct dvpoll { struct pollfd *dp_fds; int dp_nfds; int dp_timeout;};dp_fds指向一个缓冲区,供ioctl返回时存放一个pollfd结构数组。dp_nfds指定该缓冲区的大小dp_timeout指定超时,非阻塞,和不设置超时(一直阻塞)//图6.13 select的str_cli函数重写为使用/dev/poll的版本#include "unp.h"#include 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 (;;) { /* block until /dev/poll says something is ready */ 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) { /* socket is readable */ ... ... ... } else { /* input is readable */ ... ... ... } } }}

kqueue接口

kqueue是一个用于异步事件通知的系统调用,最初由FreeBSD开发。它可以监视文件描述符、定时器和信号等事件,并在这些事件发生时通知进程。相比于传统的轮询方式,kqueue能够提供更高效的事件处理机制 – GPT3

本接口允许进程向内核注册描述所关注kqueue时间的事件过滤器。

1234567891011121314151617181920#include #include #include 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 keventstruct kevent { uintptr_t ident; /* identifier (e.g, file descriptor) */ short filter; /* filter type (e.g, EVFILT_READ) */ u_short flags; /* action flags (e.g, EV_ADD) */ u_int fflags; /* filter-specific flags */ intptr_t data; /* filter-specific data */ void *udata; /* opaque user data */};

kevent函数通过eventlist参数返回

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849//图6.13 select的str_cli函数重写使用kqueue版本#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) { /* socket is readable */ 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)) { /* input is readable */ 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); /* send FIN */ kev[i].flags = EV_DELETE; Kevent(kq, &kev[i],1, NULL, 0, &ts);/* remove kevent */ continue; } } } }}

14.10 T/TCP:事务目的TCPT/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/O16.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函数(修订版)维护两个缓冲区:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141#include #include #include #include #include #include #include #include #include #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]); /* Fri Sep 13 00:00:00 1986\n\0*/ /* 012345678901234567890123 4 5*/ snprintf(str+8, sizeof(str)-8, ".%06ld", tv.tv_usec); /* gf_time 函数返回一个当前时间的字符串 格式如下*/ /* 12:34:56.123456*/ 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); /* send FIN */ } else { fprintf(stderr, "%s: read %d bytes from stdin\n", gf_time(), n); toiptr += n; //此处调用FD_SET 使得在本循环内对应位测试为真,即FD_ISSET测试成功,执行对应操作 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; /* back to beginning of buffer */ } } 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; /* back to beginning of buffer */ if (stdineof) shutdown(sockfd, SHUT_WR); /* send FIN */ } } } }}

str_cli的较简单版本

与代码的复杂性相比,使用非阻塞I/O的方式不值得。当需要使用非阻塞I/O时,更简单的办法通常是把应用程序任务划分到多个进程(fork或多线程)

12345678910111213141516171819202122#include "unp.h"void str_cli(FILE *fp, int sockfd){ pid_t pid; char sendline[MAXLINE], recvline[MAXLINE]; if ((pid = fork()) == 0) { /* child: server->stdout */ while (readline(sockfd, recvline, MAXLINE) > 0) fputs(recvline, stdout); /* in case parent still running */ kill(getppid(), SIGTERM;) exit(0); } /* parent: stdin->server */ 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)

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051#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); /* do whatever we want while the connect is taking place */ if (n == 0) goto done; /* connect completed immediately */ 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); /* timeout */ 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 非阻塞accept1234567891011121314151617181920212223242526272829303132333435363738//建立连接并发送一个RST的TCP回射客户程序#include #include #include #include #include #include #include #include #include #define SERV_PORT 7777int main(int argc, char **argv){ int sockfd; struct linger ling; struct sockaddr_in servaddr; if (argc != 2) perror("usage: tcpcli "); 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,随后关闭该套接口

接着修改服务器程序部分:

1234567if(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函数

1234#include int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void*), void *arg); //0 -- 成功 , 正Exxx值 -- 出错

tid 返回创建的线程id,attr为线程属性,func线程处理函数,arg传递给线程处理函数的参数

线程属性等详细见apue

pthread_join函数

等待一个给定线程终止。对比线程,pthread_create类似于fork,pthread_join类似于waitpid

123#include int pthread_join(pthread_t tid, void **status); //0 -- 成功 , 正Exxx值 -- 出错

status为线程返回值

pthread_self函数

123#include pthread_t pthread_self(void); //返回调用线程的线程ID

类似于getpid

pthread_detach函数

123#include int pthread_detach(pthread_t tid); //0 -- 成功 , 正Exxx值 -- 出错

简单来说,设置线程分离后,该线程不需要我们在手动回收(pthread_join),线程终止时,会将相关资源都释放

pthread_exit函数

123#include void pthread_exit(void *status); //不返回到调用者

让一个线程终止的方法之一。

让一个线程终止的另两种方法:

启动线程的函数可以返回。

进程main函数返回或者任何线程调用exit,整个进程终止。

26.3 使用线程的str_cli函数1234567891011121314151617181920212223242526272829303132#include "unpthread.h"void * copyto(void*);static int sockfd; /* global for both threads to access */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); /* EOF on stdin, send FIN */ return (NULL);}

26.4 使用线程的TCP回射服务器程序123456789101112131415161718192021222324252627282930313233#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 [] "); 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); /* same function as before */ CLose((int)arg); return (NULL);}

给线程传递参数

注意不能简单地把connfd的地址传递给新线程,如下代码所示

123456789101112131415161718192021int 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的影响发生变动,要么使用最开始的直接传值的方式,要么对每个线程分配一块空间,再由线程释放

123456789101112131415161718192021222324252627282930313233#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 线程特定数据处理将未线程化程序转换成使用线程的版本时,函数使用静态变量引起错误的问题。

1234#include int pthread_once(pthread_once_t *onceptr, void (*init)(void));int pthread_key_create(pthread_key_t *keyptr, void (*destructor)(void*value)); //0 -- 成功 , 正Exxx值 -- 出错

简单来说,pthread_key_create用来初始化线程特定数据,在进程范围内对于一个给定键,该函数只能被调用一次。pthread_once则用来确保该键只被调用一次,pthread_key_create在Key结构数组中,找到第一个未引用的元素,将它的索引返回给调用者

1234567891011121314151617181920212223242526//使用例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); /* initialize memory pointed to by ptr */ } ... /* use the values pointed to by ptr */}

pthread_getspecific 和 pthread_setspecific 用于获取和存放相关联的值

12345#include void * pthread_getspecific(pthread_key_t key); //返回:指向线程特定数据的指针int pthread_setspecific(pthread_key_t key, const void *value); //0 -- 成功 , 正Exxx值 -- 出错

图3.18函数的优化版本

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566#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节

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798#include "unpthread.h"#include #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) { /* find a file to read */ 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); /* write() thet GET conmmand */ /* Read server's reply */ 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使用的时候,可能在使用之前,该值被另一线程更改

1234#include int pthread_mutex_lock(pthread_mutex_t *mptr);int pthread_mutex_unlock(pthread_mutex_t *mptr); //0 -- 成功 , 正Exxx值 -- 出错

如果试图上锁已被另外某个线程锁住的一个互斥锁,本线程将被阻塞,直到该互斥锁被解锁为止

12345678910111213141516171819202122232425262728293031323334#include "unpthread.h"#define NLOOP 5000int 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); /* wait for both threads to terminate */ Pthread_join(tidA, NULL); Pthread_join(tidB, NULL); exit(0);}void * doit(void *vptr){ int i, val; for (i = 0; i < NLOOP; i++) { //shang's 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 条件变量与互斥锁相比,互斥锁是主动上锁,避免竞争,条件变量,等待条件满足,然后处理相关

1234#include int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);int pthread_cond_signal(pthread_cond_t *cptr); //0 -- 成功 , 正Exxx值 -- 出错

举例来说明。条件变量的使用,同时也需要用到互斥锁的功能

对于26.6 thr_join的替代

123456789101112131415161718192021222324252627282930int 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);//主循环阻塞在pthread_cond_wati调用中,等待某个即将终止的线程发送信号到与ndone关联的条件变量while (nlefttoread < 0) { while (nconn < maxnconn && nlefttoconn >0) { /* find a file to read */ ... } /* wait for one of the threads to terminate */ 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); /* update file[i] for terminated thread */ ... } } Pthread_mutex_unlock(&ndone_mutex);}

pthread_cond_wait上的原子操作:将互斥锁解锁然后把调用线程投入睡眠

另外两个与signal作用类似的

1234#include 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函数

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556//全局变量的唯一变动,增加一个新标志和一个条件变量#define F_JOINED 8int ndone;pthread_mutex_t ndone_mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t ndone_cond = PTHREAD_COND_INITIALIZER;//do_get_read函数的唯一变动是在本线程终止之前,递增ndone并通知主循环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) { /* find a file on read */ 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--; } /* wait for one of the threds to terminate */ 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更快,即体现线程在繁重使用的网络服务器上的优势。

同一进程内的所有线程共享全局变量和描述字,从而允许不同线程之间共享这些信息,但同时也引入了同步问题。

编写能够被线程化应用程序调用的函数时,这些函数必须做到线程安全。