Elementary TCP Sockets
1. Introduction
UNIX可以让一个server同时与多个client建立连接并通信. 本章会调用让server调用fork(), 为每个client创建一个进程. 通常情况下, TCP server会先启动, 之后client启动并试图连接server. Server处理完毕请求后回复client, 直到client关闭本次连接. 整个流程如下图:
2. socket Function
想要进行network I/O操作, 必须先执行socket()创建一个socket对象.
|
socket()参数所能使用的常量:
- family:
- AF_INET: IPv4 protocols
- AF_INET6: IPv6 protocols
- AF_LOCAL: UNIX domain protocols
- AF_ROUTE: Routing socket
- AF_KEY: Key socket
- type:
- SOCK_STREAM: stream socket
- SOCK_DRGAM: datagram socket
- SOCK_SEQPACKET: sequenced packet socket
- SOCK_RAW: raw socket
- protocol
- IPPROTO_TCP: TCP transport protocol
- IPPROTO_UDP: UDP transport protocol
- IPPROTO_SCTP: SCTP transport protocol
以下是family和type的可用组合:
| AF_INET | AF_INET6 | AF_LOCAL | AF_ROUTE | AF_KEY | |
|---|---|---|---|---|---|
| SOCK_STREAM | TCP,SCTP | TCP,SCTP | Yes | ||
| SOCK_DGRAM | UDP | UDP | Yes | ||
| SOCK_SEQPACKET | SCTP | SCTP | Yes | ||
| SOCK_RAW | IPv4 | IPv6 | Yes | Yes |
Protocol family分为两种: AF_xxx和PF_xxx. AF_代表address family, PF_表示protocol family. 原本计划设计一个支持多个address family的protocol family, PF_用于创建socket, AF_用于socket address structure中; 但实际上并没有实现, <sys/socket.h>中PF_与AF_值相同, 因此本例使用AF_作为family参数.
3. connect Function
connect()可用于TCP client向TCP server发起连接:
|
addr指向的socket address structure必须含有server的IP address和port. Client调用connect()之前不需要调用bind(), 内核会自动为client分配一个临时端口. connect()会初始化TCP三次握手, 只有完成连接或出错后才会返回, 以下是几种错误的可能性:
- Client发出SYN后无回复, 返回
ETIMEDOUT. - Client发出SYN后, server回复RST, 表示server指定端口没有程序运行, client返回
ECONNREFUSED. - client发出SYN后, 中间路由返回ICMP "destination unreachable". Client还会多次尝试发送SYN, 若仍没收到回复, 则返回
EHOSTUNREACH或ENETUNREACH.
RST出现情况:
- 对端指定端口没有进程运行(三次握手中)
- 对端想取消已有连接(三次握手之后)
- 对端接收到一个根本不存在的segment
connect()会将socket状态从CLOSED切换到SYN_SENT, 如果成功建立连接则切换到ESTABLISHED, 若失败则该socket不可再用, 必须被关闭.
4. bind Function
bind()会为socket分配一个local protocol address(包括一个32-bit IPv4 address或128-bit IPv6 address和一个16bit TCP或UDP port number).
|
client不需调用bind(), 内核会自动分配一个临时port. server则需手动调用bind(), 因为server必须使用固定port来保持与client的连接.
bind可以指定IP address和port number, 也可以不指定:
| IP Address | Port | Result |
|---|---|---|
| 通配符 | 0 | Kernel chooses IP address and port |
| 通配符 | nonzero | Kernel chooses IP address, specfies port |
| 本地IP地址 | 0 | Kernel specifies IP address, chooses port |
| 本地IP地址 | nonzero | Kernel sepecifies IP address and port |
对于IPv4, 通配符为INADDR_ANY;, 通知内核来选择IP地址.
struct sockaddr_in servaddr; |
对于IPv6, 通配符为in6addr_any:
struct sockaddr_in6 serv; |
bind()不会返回临时分配的port number, 需调用getsockname()来获取protocol address. bind()最常返回的错误为 EADDRINUSE, 表示address已被使用.
5. listen Function
listen()只会被TCP server调用.
- 当socket被创建后会即可向外发送请求,
listen()可将未建立连接的socket变为passive socket, 将socket状态从CLOSED切换为LISTEN. listen()的backlog参数表示kernel为socket保留的最大连接数量
|
Kernel为passoive socket维护了两个队列, 两个队列长度之和即为backlog:
- Incomplete connection queue: 收到SYN但还没完成YCP三次握手, socket位于
SYN_RCVD状态 - Completed connection queue: 完成TCP三次握手, socket位于
ESTABLISHED状态
以下是kernel维护的两个queue:
以下是TCP三次握手的流程:
当server收到client的SYN时, 会将该请求加入到incompleted queue并回复ACK. 若ACK回复超时, 则将该请求从imcomplete queue中移除; 若收到client发来的SYN-ACK后, 将该请求从imcomplete queue移至completed queue尾部. 进程调用accpet()会从completed queue取出并处理首部第一个请求; 若completed queue为空, 则进程进入睡眠状态.
backlog的数值一般为5, 但并不适用于现在网络, Berkeley-derived系统为backlog添加了模糊因子(乘以1.5). 不要将backlog定义为0. 因为各系统对于0都有不同的处理方式, 若不想接受任何请求, 直接关闭passive socket.
在不考虑报文丢失或重传的情况下, 连接请求在imcompleted queue停留的时间为1 RTT.
当queue已经占满时, TCP server会忽略client发送的SYN且不发送RST. 因为拥塞是暂时的, 且client会再次发送SYN来请求连接. 如果server回复RST, 则client所调用的connect()会报错; 而不回复RST可让client的connect()自动重发SYN.
6. accept Function
Server调用accept()从completed queue的首部取出一个请求. 若completed queue为空, 则进程进入睡眠.
|
accpet()成功调用后返回一个全新的file descriptor, 表示与client的TCP connection. 因此accept()实际上返回以下三个数据:
- return value: 表示与client的TCP connection
- 参数addr: 表示client的protocol address
- 参数len: 表示参数addr的大小
7. fork and exec Functions
在创建concurrent server之前, 必须先学习fork()和exec()的使用
7.1 fork Function
|
fork()虽然只调用一次, 但会返回两个值. 因此调用fork()需使用if来判断当前进程为父进程还是子进程. 之所以子进程中的fork()不返回父进程PID, 是因为getppid()可获取当前进程的父进程PID; 但由于一个进程可拥有多个子进程, 因此父进程需通过fork()获取子进程的PID.fork()可用于server的并发处理: server在成功调用accept()后调用fork(), 父进程和子进程会共享connected socket: 子进程负责读取和写入connected socket, 父进程负责关闭connected socket. 以下是fork()的两种用法:
- 进程调用
fork()复制自身, 并使用子进程完成其他任务, 如: network server - 进程调用
fork(),在子进程中调用exec()替代原本进程数据来执行其他任务, 如: shell执行程序
7.2 exec Function
exec有6种不同的函数表示, 区别如下:
- 执行的程序目录: filename或pathname
- 执行程序的参数: 可变参数或数组指针
- 执行程序的环境变量: 使用当前进程的环境变量或指定新的环境变量
|
以下是6种exec函数的关系图:
8. Concurrent Servers
若sever需服务多个client, 最简单的方式就是让server为每个client调用fork()处理请求:
pid_t pid; |
当accept()返回后, 说明server与client成功建立连接. 这时调用fork()让子进程与父进程共享connfd: 子进程负责与client的通信, 父进程负责等待新的连接.
对TCP connection调用close()会向client发送FIN终止连接, 上述代码中子进程父进程虽然分别调用close(), 但并不会发送两次FIN: 每个file或socket都有一个reference count, 当server调用fork()后, connfd的refernece count会从1变为2, 每次调用close()都会使该socket的reference count减1, 只有当reference count变为0时才会向client发送FIN. 因此只有最后一次调用close()才会发送FIN.
以下是client与server之间的连接状态流程:
server调用
accept()之前:
server调用
accept()后:
server调用
fork()后:
父进程调用
close(connfd), 子进程调用close(listenfd)后
9. close Function
close()用于关闭socket并中止TCP连接.
|
调用close()后, server会继续将未发送完毕的数据发送给client, 之后再进行TCP four-packet connection termination. 如果需要强制关闭一段connection, 可调用shutdown(). 不调用close()也很危险, 每个系统的file descriptor都有数量上限, 不调用close()会导致connected socket用光所有file descriptor; 而且不调用close()会让TCP connection一直保持连接状态.
10. getsockname and getpeername Functions
getsockname()和getpeername()用于获取socket的local protocol address和foreign protocol address.
|
以下是getsockname()和getpeername()的使用情景:
client调用
connect()后可通过getsockname()获取当前connection使用的local IP address和port number.server在调用
bind()时使用0作为port number, 可调用getsockname()获取kernel自动分配的local port number.getsockname()也可用于获取address family:int sockfd_to_family(int sockfd)
{
struct sockaddr_storage ss;
socklen_t len;
len = sizeof(ss);
if (getsockname(sockfd, (SA *) &ss, &len) < 0)
return(-1);
return(ss.ss_family);
}server在调用
bind()时将通配符作为IP address, 可调用getsockname()获取kernel自动分配的IP addressserver调用
accept()后, 调用fork()复制自身并执行exec来运行其他程序, 这时只能通过调用getpeername()来获取client的protocol address, 因为exec会抹除子进程中的所有内存数据, 包括foreign protocol address. 如下图:
调用exec后, connfd会和peer's address一样被清除. 但调用
getpeername()时又必须使用connfd. 因此在调用exec时可将connfd作为参数传入, 或者在调用exec之前为connfd创建一个descriptor.