Unix 网络编程卷一阅读随笔。
[C2.4, 35] TCP 客户先与某个给定服务器建立一个连接(三次握手),再跨该连接与那个服务器交换数据,然后终止这个连接(四次挥手)。
[C2.4, 37-39] 结合 socket 网络编程接口,TCP 的三次握手与四次挥手:
服务器被动打开 :服务器首先调用socket()
函数创建一个套接字,然后调用 bind()
函数将该套接字绑定到一个本地地址和端口。接着,服务器调用 listen()
函数使该套接字进入监听状态,准备好接受外来的连接。
服务器调用 accept()
:服务器调用accept()
函数,该函数阻塞等待客户端的连接请求。当有客户端请求连接时,accept()
返回一个新的套接字描述符,用于与客户端进行通信。原来的监听套接字继续保持监听状态。
客户端发起主动打开 :客户端调用socket()
函数创建一个套接字,然后调用 connect()
函数向服务器发起连接请求。此时,客户端和服务器进行 TCP 的三次握手:
服务器 accept 返回 :当服务器收到客户端的连接请求(即 SYN 包)后,accept()
函数返回,提供一个新的套接字描述符用于与客户端通信。此时,三次握手完成,客户端和服务器可以开始通信。
数据传输:连接建立后,客户端和服务器可以通过套接字进行数据传输。服务器一般调用read()
(或recv()
)函数阻塞等待接收数据,客户端调用write()
(或send()
)函数发送数据。
连接关闭 :数据传输完成后, 通常 由客户端调用 close()
函数发起主动关闭连接,进行四次挥手:
close()
函数,发送 FIN 包(第三次挥手),表示准备关闭连接。客户端进入 TIME_WAIT 状态,等待一定时间后彻底关闭连接,确保服务器收到 ACK 包。
无论是客户端还是服务器,任何一端都可以调用
close()
执行主动关闭。通常情况是客户端执行主动关闭,但是某些协议(比如 HTTP/1.0)却是由服务器执行主动关闭。
结合 这篇文章 的 TCP 三次握手和四次挥手一起理解上图和下图。
上图中,服务器对客户端请求的 ACK 确认是伴随着其应答消息一起发送的,这种做法称为捎带(piggybacking)。它通常在服务器处理请求并产生应答的时间少于 200ms 时发生。如果处理耗时更长,如 1s,那么我们看到的将先是 ACK 确认,后是应答。
IPv4 套接字地址:struct sockaddr_in {}
IPv6 套接字地址:struct sockaddr_in6 {}
通用套接字地址:struct sockaddr {}
当某种套接字地址作为一个参数传递给任何套接字函数时,套接字总是以引用的形式来传递,且 这些套接字函数必须能处理任意协议族的套接字地址结构 。故此,需要将它们强转转换成通用套接字地址,传递给这些函数。这些函数有:bind(), connect(), accept() 等。
[C3.3, 73-74] 有时候,应用进程需要告诉内核套接字地址的长度(内核就知道从进程复制多少数据进来了,这样内核在操作该结构时不至于越界),且内核需要告诉应用进程套接字地址的长度(它告诉进程内核在该结构中究竟存储了多少数据)。这就需要值 - 结果类型的参数,传入指向地址长度的指针、返回地址长度的结果再存储在这个指针中,供应用进程使用。
[C3.6, 81-83]
ASCII 字符串格式的 IP 与二进制格式的网络地址相互转换:
IPv4 ASCII 到 Network 地址:inet_aton(), inet_addr()
Network 地址到 IPv4 ASCII:inet_ntoa()
使用于 IPv4 和 IPv6 的新的转换函数,p=presentation, n=numeric:
ASCII 到 Network 地址:inet_pton()
Network 地址到 ASCII:inet_ntop()
[C3.9, 88-89]
对于字节流套接字(如 TCP 套接字)上的 read()和 write()函数所表现的行为不同于通常的文件 I/O。字节流套接字上调用 read()或 write(),输入或输出的字节数可能比实际请求的数量少,但这不表示发生了错误。
这个现象的原因是:内核中用于套接字的缓冲区可能已达到了极限。此时需要调用者再次调用 read()或 write()函数,以输入或输出剩余的字节。
你是否有印象,好多 read()都在 whlie 循环下执行,除了由于网络的不确定性和延迟,也有上述原因(个人理解)。
[C4.3, 99-100]
在客户端调用 connect()函数前不必非得调用 bind()函数——因为如果需要的话,内核会确定源 IP 地址,并选择一个临时端口作为源端口。
[C4.4, 100-102]
调用 connect()函数将触发 TCP 三次握手,而且仅在连接建立成功或出错时才返回。
bind()函数把一个本地协议地址赋予一个套接字。
如果客户端或服务器不绑定 IP 地址,会怎么处理?
listen()函数仅由 TCP 服务器调用。
当 socket()函数创建一个套接字时,它被假设为一个主动套接字。也就是说,它被假设为是一个将调用 connect()发起连接的客户套接字。但是,在服务端没有 connect()调用,listen()将一个未 connect 的套接字转换成一个被动套接字,指示内核应该接收指向该套接字的连接请求。
listen()监听最大 backlog 个连接,其中 backlog 是未完成连接队列个数(未完成 TCP 三次握手的分节在此队列)和已完成连接队列(已完成 TCP 三次握手的分节在此队列)个数之和。若分节完成三次握手,则将被转移至已完成连接队列的队尾。
accept()函数仅由 TCP 服务器调用,用于从已完成连接队列的队头返回下一个已完成连接的客户套接字(返回值称为已连接套接字,accept 第一个参数称为监听套接字)。
Unix 系统中派生新进程的唯一方法——fork()及其变体。
在进程中调用一次 fork(),它却返回两次:
子进程的返回值是 0,是因为子进程的父进程唯一,它可以通过 getpid()获取父进程的 PID。而父进程只能通过记录返回值来存储多个子进程的 PID。
父进程中调用 fork()之前打开的所有描述符,在 fork()返回之后,可在父进程与子进程之间共享。
利用这个特性,在网络服务器中,可在 accept()返回客户套接字后,fork()一个子进程,由子进程处理这个客户的请求,而父进程可关闭这个客户套接字,从而可以再次 accept()其它连接请求。
对于“父进程可关闭这个客户套接字”,那么为什么子进程中还能使用这个客户套接字?
这是因为描述符(文件或套接字)都有一个引用计数。引用计数在文件表项中维护,它是当前打开着的描述符的个数。
当 fork()返回后,父进程中先前打开的所有描述符,会在父进程与子进程间共享(也就是被复制),即每个描述符的引用计数都会加 1。因此,一个描述符真正的清理和资源释放要等到其引用计数值达到 0 时才发生。
使用 name 一词令人误解,getsockname()和 getpeername()函数返回是本端或对端的协议地址——对于 IP 协议来说,就是 IP 地址和端口号。
需要这两个函数理由: