这本书我挑选了其中关于TCP、UDP的部分章节来看,跳过了SCTP部分。里面有些小程序还是值得自己一写的,对于模型:比如可以从简单的阻塞型I/O,到I/O复用,再到使用进程线程等等内容……再比如可以当做练手的项目的内容有:简单版的ping、traceroute程序,还有基于UDP的可靠传输协议等等……
======================================================================================================
以下是我的读书笔记:
unix网络编程
ch3 套接字编程简介
1. IPv4套接字地址结构:
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
structin_addr sin_addr;
};
Posix规范只需要这三个字段,定义额外的字段是可接受的。地址总是采用网络字节序(大端)来存储的。
2. 通用套接字地址结构:
struct sockaddr{
uint8_t sa_len;
sa_family_t sa_family;
char sa_data[14];
};
3. 进程向内核传递套接字地址结构的函数:bind,connect,sendto,另一个参数是该结构的整数大小。
内核向进程传递套接字地址结构的函数:accept,recvfrom,getsockname,getpeername,另一个参数时指向该结构大小的整数变量的指针。
4.
5. 使用这些函数可以不用关心主机字节序和网络字节序的真实值,要做的只是调用适当的函数在主机和网络字节序之间转换某个值。
6. voidbzero(void *dest, size_t nbytes)把目标字符串中指定数目的字节置为0。
7. 网络字节顺序是TCP/IP中规定好的一种数据表示格式,它与具体的CPU类型、操作系统等无关,从而可以保证数据在不同主机之间传输时能够被正确解释。
int inet_aton(const char *strptr, structin_addr *addrptr);将strptr所指C字符串转换成一个32位网络字节序二进制值,并通过addrptr来存储,成功返回1,失败0。
char *inet_ntoa(struct in_addr inaddr);将网络字节序转换为对应点分十进制数串。
ch4 基本套接字编程
1. 三次握手的意义:
a) 确保2端同时建立连接(可以假设A发送的第一个SYN在网络中长时间滞留,延误到B,B回应ACK,但是A不予理睬,所以连接并未因此建立)
2. 四次挥手中TIME_WAIT状态的意义:
a) 保证A发送的最后一个ACK到达B,若B超时没收到,再发一个FIN,A收到这个FIN后重启2MLS定时器。(这个定时器时间至少是1MSL)
b) 允许老的重复分组在网络中消逝。
3. intsocket(int family, int type, int protocol);最后一个参数如果为0表明选择所给定family和type组合的系统默认值。第二个参数通常为:SOCK_STREAM,SOCK_DGRAM
4. intconnect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);成功返回0,出错-1。调用此函数,将激发三次握手。
5. intbind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);返回同上
a) 进程可以把一个特定IP绑定在它的套接字上,相当于指定了源IP地址。但进程通常不这么做,通常是内核根据外出网络接口选择源IP地址。
b) 服务器指定端口号为0,内核就在bind调用时选择一个临时端口。INADDR_ANY为IP通配地址,等TCP已连接或发出UDP数据报才选择一个本地IP地址。进程捆绑多个非通配IP地址到同一个套接字的方法是,把这个IP地址设置为同一个别名。
6. intlisten(int sockfd, int backlog);
a) backlog包括一个未完成连接队列和一个已完成连接队列(完成三次握手),取值为0~5,accept函数就是从这个队列头返回下一个已连接队列。如果这个队列是满的,当客户的SYN到达时,TCP就忽略分节,而不是回应RST!
7. intaccept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);从已完成连接队列头返回下一个已完成连接。若该队列为空,进程将被投入睡眠。若成功为这个客户创建一个已连接套接字,注意与监听套接字区别。不产生网络通讯。
a) 故假设服务器若只有监听,没有accept,客户端connect仍然会三次握手成功。客户端仍能write,存储在TCP接收缓冲区,待服务器accept后还可以读取。若服务器始终没有接受,客户端不能正常断开连接,只有前2个报文:FIN和ACK(服务器无法得到已连接套接字,未调用close),一直占用资源直到TIME_WAIT2状态超时。
8. #include<unistd.h>
pid_t fork(void); 在子进程中返回0,父进程中为子进程ID,出错-1。
a) 所有子进程只有一个父进程,我们可以在子进程中getppid得到父进程ID
b) 一个进程创建一个自身的副本,父子进程从fork处继续执行,但是先后顺序不一定。
c) 一个进程如果要执行另一个程序,首先调用fork,在其中一个副本调用exec(调用进程)把自身替换成新的程序(新程序),该新程序从main函数开始执行,PID不变。这些exec函数只有出错才返回到调用者,否则控制将被传递到新程序的起始点。描述符通常跨exec继续保持打开。
d) listenfd和connfd描述符在父子进程间被共享(大部分栈和缓冲区被复制,引用计数变为2),且套接字真正的清理和资源释放要等到引用计数值达到0时才发生。注意:虽然父子进程共享代码空间,但是涉及写数据时子进程有自己的数据空间,在有数据修改时,系统会为子进程申请新的页面。
9. #include<unistd.h>
int close(int sockfd); 成功返回0,出错则为-1。close的默认行为是把套接字标记成已关闭,即不能再作为read和write的第一个参数了,资源真正被释放在描述符引用计数变为0的时候。在并发服务器中,父进程对每一个已经连接套接字要close,子进程结束后也要再close一次才算真正关闭。不然会导致描述符耗尽。
10.
a) 对未bind本地IP和端口的TCP客户,connect成功后,可调用getsockname获取内核赋予该连接的本地IP和端口号。
b) 服务器在以端口0调用bind后,调用getsockname获取内核赋予的端口号。
可用来获取地址族。对bind通配IP地址的TCP服务器,accept成功返回后getsockname返回由内核赋予该连接的本地IP地址。
c) 当服务器调用accept后的某进程通过exec执行程序,获取客户身份的唯一途径是getpeername。描述符跨exec保持开放,对端地址丢失。
11. 对recv等函数成功时返回实际读取到的数据长度,可能小于我们期望的长度,因此需要多次调用,返回0意味着对端发送FIN,出错时返回-1。
ch5 TCP客户/服务器程序示例
1. unix信号默认是不排队的。一旦安装了信号处理函数,便一直安装着。在这个信号处理函数运行期间,正被递交的信号是阻塞的。
2. 处理僵死进程:在信号处理函数中,不断调用waitpid,相比较wait的优点是,在有尚未终止的子进程在运行时可以不要阻塞。
3. 处理被中断的系统调用:慢系统调用指调用可能永远无法返回。当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,可能返回EINTR错误,我们要准备好处理重启被中断的系统调用,accept、read、write、select、open都是合适的。而对于connect必须调用select等待连接完成。
4. 特殊情况:
a) 调用accept之前收到RST
b) 服务器进程终止,服务器主机关闭:均发送FIN
c) 服务器主机崩溃后重启:客户再发数据会收到RST,客户端如果再发,进程就产生一个SIGPIPE信号
5. 穿越套接字传送二进制结构绝不明智,通常:
a) 所有数值作为文本串传送
b) 显示定义所支持二进制格式(大端,小端,位数)
ch6 I/O复用:select和poll函数
1. 阻塞式I/O模型
使用UDP而不是TCP的原因是:数据准备好读取概念较简单,要么整个已经收到,要么没有。对于TCP来说还有低水位标记等额外变量作用,导致概念复杂化。
2. 非阻塞I/O模型
请求的I/O操作未准备好时,不投入睡眠,而是返回一个错误EWOULDBLOCK,也称轮询。
3. I/O复用模型
应用场合:
a) 客户处理多个描述符或套接字
b) 服务器既要监听,又要处理已连接套接字
c) 服务器同时处理UDP和TCP,或者多个服务和协议。
4. 信号驱动式I/O模型
内核在描述符就绪时就发送SIGIO信号通知我们,模型的优势在于等待数据报到达期间进程不被阻塞。
5. 异步I/O模型
调用aio_read函数给内核传递描述符、缓冲区指针、缓冲区大小、文件偏移,并告诉内核在整个操作完成(数据从内核复制到用户空间)通知我们。
6. #include<sys/select.h> #include<sys/time.h>
intselect(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset,
conststruct timeval *timeout);
maxfdp1为待测试最大描述符个数(最大编号+1)
timeout表等待任何一个描述符就绪可花多长时间,永远等待NULL,等待一定时间,根本不等待设置结构体2个值为0。
select使用描述符集,通常是一个整数数组,每个整数的每一位对应一个描述符。
返回:若有就绪描述符则为就绪的数目,超时为0,错误-1
voidFD_ZERO(fd_set *fdset); //必须初始化,负责后果不可预计
voidFD_SET(int fd, fd_set *fdset);
voidFD_CLR(int fd, fd_set *fd_set);
voidFD_ISSET(int fd, fd_set *fd_set);
7. 描述符就绪条件
a) 套接字准备好读:
i. 该套接字接收缓冲区字节数大于等于该套接字低水位标记大小。
ii. 该套接字为监听套接字且已完成的连接数不为0。
iii. 有一个套接字错误待处理。
b) 套接字准备好写:
i. 该套接字发送缓冲区可用字节数大于等于低水位标记大小。
ii. 使用非阻塞connect的套接字已建立连接。
iii. 套接字错误待处理。
8. ch5中的标准输入中的EOF并不意味着完成了套接字的读入,可能仍有请求或应答在路上。因此客户端应采用Shutdown发送一个FIN来关闭连接。只有当标准输入发送FIN,可读套接字收到EOF,才能退出程序。
intshutdown(int sockfd, int howto);
howto有三个值:SHUT_RD SHUT_WR SHUT_RDWR
9. 当一个服务器处理多个客户时绝对不能阻塞与只与单个客户相关的某个函数调用。
10. #include<poll.h>
int poll(struct pollfd *fdarray, unsigned longnfds, int timeout);
nfds表连接个数,返回:若有就绪描述符则返回其数目,超时为0,出错为-1。
struct pollfd {
int fd;
short events; //调用值
short revents; //返回值
} 主要分为处理输入的四个常值,处理输出的三个常值,处理错误的三个常值。
poll识别三类数据:普通,优先级带,高优先级。通过revents来确定是新客户还是新数据。具体见P145
11. netstat-rn
route
netstat -a | grep 9877
12. 使用poll的TCP回射服务器P146
使用select的TCP服务器程序P141
使用select正确处理EOF的str_cli函数P137
TCP客户端见P100
ch8 基本UDP套接字编程
1. UDP是无连接不可靠的数据报协议,TCP提供面向连接的可靠字节流。
2. a)ssize_t recvfrom(int sockfd, void *buff,size_t nbytes, int flags,
structsockaddr *from, socklen_t *addrlen );
其中nbytes指发送缓冲区参数,addrlen是一个值-结果参数。
b) ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags,
conststruct sockaddr *to, socklen_t addrlen);
其中nbytes指发送字节数。
flags:调用操作方式。是一个或者多个标志的组合体,可通过or操作连在一起。
若成功则返回读或写的字节数;出错为-1。
注意:UDP中写长度为0的数据报是可行,recvfrom返回0也是可接受的,而在TCP中read返回0意味着关闭连接。 (#include sys/socket.h)
3. UDP层有隐含排队发生,每个套接字都有一个接受缓冲区FIFO队列,可以通过SO_RECVBUF套接字选项改变。同时为了防止客户永久阻塞在recvfrom,通常设置一个超时。
4. 弱端系统模型:IP实现接受目的地址为本主机任一IP地址的数据报而不管数据报到达的接口。强端系统模型只接受接口和目的地址一致的数据报。
5. recvfrom可以返回的信息仅有errno值,没办法返回出错数据报目的IP和端口。因此仅在进程已将UDP套接字连接到恰恰一个对端后,异步错误才返回给进程。这个ICMP出错信息包含引起错误的数据报的IP首部和UDP首部。
6. 客户必须给sento指定服务器IP和端口号,一般来说客户的IP和端口由内核自动选择(客户未绑定),临时端口不能改变,IP则可基于路由可能改变;如果客户绑定了一个IP地址,但数据报必须从另外一个数据链路走,则IP数据报的源地址还是我们绑定的。
7.
8. connect:不给对端发任何信息,完全是一个本地操作,只保存对端IP地址和端口号。对于调用了connect的已连接套接字:
a) 不能指定目的IP和端口,即不使用sendto,而调用write和send。
b) 不必使用recvfrom获得发送者,而用read,recv或recvmsg
c) 已连接UDP套接字引发的异步错误会返回给他们所在进程。
9. 给一个已连接UDP套接字再次调用connect:
a) 指定新的IP和端口号
b) 断开套接字,connect中把地址族设置为AF_UNSPEC
10. 当应用进程知道自己要给同一目的发送多个数据报时,显式连接套接字效率更高,内核只复制一次含目的IP和端口号的套接字地址结构。临时连接未连接套接字耗费每个UDP 传输1/3开销。
11. Setsockopt(sockfd,SOL_SOCKET, SO_RCVBUF, &n, sizeof(n));n为缓冲区新的大小。
12. connect带来的一个好处是可以确定输出接口的地址,通过Getsockname(sockfd, (SA *) &cliaddr, &len),然后打印Sock_ntop((SA *)&cliaddr, len)。
13. 监听套接字也有自己的缓冲区,但不接受数据。大多数实现并不预先分配发送、接收缓冲区大小。SO_SNBUF和SO_RCVBUF仅改变上限值。
14. 理论上,UDP可sendto的最大数据报为65535-20-8字节,但实际还要考虑很多因素,如MTU,接收缓冲区大小等等。
15. 使用select处理TCP和UDP的回射服务器程序P205
UDP回射客户端P190,未绑定目的地址 dg_cli修订版P199 connet
ch16 高级I/O函数及非阻塞I/O
1. I/O操作上设置套接字超时的方法:
a) 调用alarm,超时满时产生SIGALRM信号
b) select中阻塞等待I/O
c) 使用套接字选项SO_RCVTIMEOHE和SO_SNDTIMEO
2. recv和send允许通过第四个参数从进程到内核传递标志。readv和witrev具有分散读和集中写的功能。recvmsg和sendmsg最通用,可以替换前面所有调用。
3. 可能阻塞的套接字调用:
a) 输入操作,read,recv等共5个函数。阻塞型无输入进入睡眠;非阻塞无输入,返回EWOULDBLOCK。
b) 输出操作,write,send等5个。
c) 接受外来连接,accept函数。
d) 发起外来连接,connect函数。connect函数一直要等到客户收到自己的SYN的ACK为止才返回。即至少阻塞一个RTT时间。对非阻塞套接字调用connect,并且连接不能立刻建立,那连接的建立能够立刻发起,但返回EINPROGRESS错误。
4. 非阻塞读和写:str_cli函数修订版(使用select)
a) 维护2个缓冲区:标准输入到套接字的缓冲区和套接字到标准输出的缓冲区
b) 按照缓冲区select描述符现在分为4块:(自己管理缓冲区,只有缓冲区条件满足才FD_SET)
i. 服务器未关闭且发送缓冲区有空闲空间(有数据输入时表明可以读入到缓冲区)
ii. 接收缓冲区有空闲空间(套接字可读时表明可以读)
iii. 有要发往服务器的数据(可以立即发送)
iv. 接收缓冲区有要发往标准输出的数据(可以显示出来)
5. snprintf(str+8,sizeof(str)-8, “.%06ld”, tv.tv.usec)
表明将tv.tvsec按照前面一个参数的格式写入到以str+8为起始地址的字符串中去,成功则返回写入字符串的长度(第二个参数);若长度小于第二个参数,则全部复制过去,再添加一个’\0’,若长度大于第二个参数,则从左到右进行截取。
6. 当我们要使用非阻塞I/O的时候,更简单的方法通常是把应用程序任务划分到多个线程或多个进程。P350程序:
a) 由子进程负责从套接字读和显示到标准输出,父进程负责读取标准输入和写套接字。
b) 若服务器发送FIN,子进程需要调用kill关闭父进程
7. 非阻塞connect:若连接不能立刻建立,SYN照样发起,返回EINPROGRESS错误
a) 用途:
i. 将三次握手叠加到其他处理上。
ii. 同时建立多个连接
iii. 程序想要一个更短的超时时间
b) 特点:
i. 若连接到的服务器在同个主机,连接立刻建立
ii. 连接成功时,描述符变为可写
iii. 连接错误时,描述符即可读又可写
iv. 如果connect在TCP三次握手前被中断,系统返回EINTR,我们不能调用connect等待未完成的连接继续完成,这种情况下,只能调用select,连接建立成功时select返回套接字可写条件。
c) 实现:
i. 调用Fcntl获取旧描述符标志,再一起设置为非阻塞:
flags = Fcntl(sockfd, F_GETFL, 0);
Fcntl(sockfd, F_SETFL, flags | O_NOBLOCK);
ii. connect(fd, (SA *)servaddr,sizeof(servaddr));成功返回0,失败返回负数,
iii. getsockopt(fd, SOL_SOCKET, SO_ERROR, &error,&n);非阻塞connect完成,且描述符可读或可写, 调用该函数获取套接字的待处理错误,返回值为0表连接成功建立。
注意非阻塞connect的移植性:如何判断是否connect成功
(1)getsockopt的SO_ERROR选项
(2)参数为0调用read
(3)再调用connect一次,若返回EISCONN表明成功
8. 当客户在服务器调用accept之前终止某个连接(客户发RST),服务器(select)会一直阻塞在accept调用上,直到有另一个客户建立一个连接(服务器select无法读其他套接字)。解决方法:
a) 把监听套接字设置为非阻塞
b) 后续accept调用忽略以下错误:EWOULDBLOCK,EINTR
参考P356Web客户程序
非阻塞I/O,使用2个进程 P350程序
ch22 高级UDP套接字编程
1. 在服务器端获得客户端的目的IP地址和接口索引方法是:
通过recvmsg中的msg中的辅助信息获得我们所需内容。cmsghdr{}包含首部加数据,即从其中数据部分提取出需要的信息,包括IP地址和端口索引,存入自己定义的结构体。要求支持IP_RECVSTADDR和IP_RECVIF选项。
2. 对数据报截断的处理法可能如下:
a) 丢弃超出部分字节,返回MSG_TRUNC标志
b) 丢弃超出字节,但不告知应用
c) 保留超出部分的字节,并在同个套接字后续读操作返回
3. 无连接的用户数据报UDP和面向连接的字节流TCP:
a) 广播和多播必须使用UDP,UDP没有连接带来的损耗
b) 对简单的请求-应答程序可使用UDP,错误检测功能必须加到应用程序内部
c) TCP包括正面确认,丢失重传,重复检测,分组排序;窗口流量控制;慢启动,拥塞避免。
4. 处理竞争(比如我们的本意是alarm能在recvfrom循环内,接受完所有数据后通过alarm跳出,但假设他在其他地方提前产生信号就永远无法返回了)的方法之一(更好的方法是利用IPC):
若该函数由下面的长跳转引起,则返回1,继续发送数据
5. 给UDP添加序号和超时重传,程序流程:
a) 发送后,记录时间戳,alarm定时器启动
b) 超时处理,重新发送
c) 通过序列号,报文长度判断接收到的是否是预期数据,从而确定等待对方重传,还是计算下一个RTO。
6. 为获得UDP应用某个数据报在何时以及那个接口到达,通常采用get_ifi_info捆绑接口地址,需要捆绑单播地址,多播地址和通配地址。
7. UDP并发服务器的两种类型:
a) 读入第一个客户请求并应答后,fork一个子进程处理C/S之间的剩余数据报。
b) inetd创建一个新的套接字,bind一个临时接口,使用该套接字发送所有应答:
P471开始的程序
其他关于线程的部分放在下一本书《Linux高性能服务器编程》