TCP客户端和服务端所需的基本套接字。服务器先启动,之后的某个时刻客户端启动并试图连接到服务器。之后客户端向服务器发送请求,服务器处理请求,并给客户端一个响应。该过程一直持续下去,直到客户端关闭,给服务端发送EOF(文件结束),服务器也关闭连接的服务器端,然后结束运行或者等待新的客户发起连接请求。如图1所示:
图1 TCP网络套接字示意图
在图中涉及到不同的函数,接下来进行详细的介绍。
socket函数
为了进行网络I/O,进程首先需要调用socket函数,指定使用的通信协议类型(IPv4的TCP、IPv6的UDP、Inux域字节流协议等)。
#include<sys/socket.h>
int socket(int family, int type, int protocol);
返回:若成功返回非负数,若失败返回-1
family表示协议族,协议族取值如表1所示:
family | 说明 |
AF_INET | IPv4协议 |
AF_INET6 | IPv6协议 |
AF_LOCAL | Unix域协议 |
AF_ROUTE | 路由套接字 |
AF_KEY | 密钥套接字 |
表1 协议族family取值
type表示套接字类型,套接字类型type如表2所示:
type | 说明 |
SOCK_STREAM | 字节流套接字 |
SOCK_DGRAM | 数据报套接字 |
SOCK_SEQPACKET | 有序分组套接字 |
SOCK_RAW | 原始套接字 |
表2 套接字类型
protocol表示某个协议类型常值,或者设置为0,以选择family和type组合的系统默认值,但并不是所有的family和type组合都是有效的,表3给出了正确组合。
表3 偷来的截图
socket函数调用成功后返回一个小的非负整数值,称为套接字描述符(socket descriptor),简称sockfd。指定了协议族(IPv4、Ipv6或Unix)和套接字类型(字节流、数据报或原始套接字),并没有指定本地协议地址或远程协议地址。
connect函数
TCP客户端使用connect函数来建立与TCP服务器之间的连接。
#include<sys/socket.h>
int connect(int sockfd, const struct *servaddr, socklen_t addrlen);
返回:若成功返回0,若失败返回-1
sockfd:socket函数返回的套接字描述符
servaddr:套接字地址结构的指针
addrlen:套接字地址结构的大小
套接字地址结构必须含有服务器的IP地址和端口号。客户端在调用connect函数前不必非要调用bind函数,因为如果需要的话,内核会确认源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接字,调用connect函数会激发TCP三次握手,而且仅在连接建立成功或失败时才会返回。
bind函数
bind函数将一个本地协议地址赋予一个套接字,对于网际协议,协议地址是32位的Ipv4地址或128位的IPv6地址与16位的TCP或UDP端口号的组合。
#include<sys/socket.h>
int bind(int sockfd, const struct *myaddr, socklen_t addrlen);
返回:若成功返回0,若失败返回-1
sockfd:socket函数返回的套接字描述符
servaddr:套接字地址结构的指针
addrlen:套接字地址结构的大小
对于TCP,调用bind函数可以指定一个端口号和一个IP地址,也可以不指定。
服务器在启动时绑定它们的众所周知的端口,当调用connect或listen的时候,内核会为相应的套接字选择一个临时端口。让内核选择临时端口对于TCP客户端来说很正常,除非应用需要一个预留端口,然而对于TCP服务器来说却极为罕见,因为服务器是通过它们众所周知的端口被大家认识的。
(例外情况:RPC服务器,它们通常就由内核为它们的监听套接字选择一个临时端口,而该端口随后通过RPC端口映射器进行注册。客户在connect这些服务器之前,必须与端口映射器联系来获取它们的临时端口,这种情况也是用与UDP的RPC服务器)
进程可以将一个特定的IP地址绑定到它的套接字上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户端,这就为在该套接字上发送的IP数据报指派了源IP地址,对于TCP服务器,这就限定该套接字只接收那些目的地为这个IP地址的客户连接。TCP客户通常不把IP地址绑定到套接字上。当连接套接字的时候,内核将根据所用外出网络接口选择源IP地址,而所有外出接口则取决于到服务器所需的路径。如果TCP服务器没有把IP地址绑定到套接字上,内核就把客户端发送的SYN的目的IP地址作为服务器的源IP地址。
如果指定端口为0,那么内核就在bind被调用时选择一个临时端口。然而如果指定IP地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地IP地址。
对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0。它告知内核去选择IP地址。
listen函数
#include<sys/socket.h>
int listen(int sockfd, int backlog);
返回:若成功返回0,若失败返回-1
sockfd:socket函数返回的套接字描述符
backlog:内核应为响应套接字排队的最大连接个数
listen函数通常在调用socket函数和bind函数之后,调用accept函数之前调用。
listen函数仅由TCP服务器调用,它做两件事情:
当socket函数创建一个套接字的时候,它被假设为一个主动套接字,也就是说,它是一个将调用connect发起连接的客户端套接字。listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接受指向该套接字的连接请求。调用listen函数使得套接字从CLOSED状态转到LISTEN状态。
图2 TCP状态转换图
为了理解backlog参数,必须为内核维护两个队列:
(1)未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由某个客户端发起并达到服务器,而服务器正在等待完成相应的TCP三次握手过程。这些套接字处于SYN_RCVD状态(见图2);
(2)已完成连接队列(completed connection queue),每个已完成TCP三次握手过程的客户端对应其中一项,这些套接字处于ESTABLISHED状态(见图2)。
accept函数
accept函数由TCP服务器调用,用于从已完成连接队列的头部返回下一个已连接,如果已连接队列为空,那么进程休眠(如果套接字是默认的阻塞方式)。
#include<sys/socket.h>
int accept(int sockfd, struct *cliaddr, socklen_t *addrlen);
返回:若成功返回非负描述符,若失败返回-1
sockfd:socket函数返回的套接字描述符
cliaddr:返回已连接的对端(客户端)的协议地址
addrlen:值-结果参数(调用前,引用前,置为由cliaddr所指的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数)
fork和exec函数
fork函数是unix派生新进程的唯一方法。
#include<unistd.h>
pid_t fork(void);
返回:在子进程中为0,在父进程中为子进程ID,若失败返回-1
fork函数调用一次,会返回两次。在父进程(调用进程)中返回一次,返回值是子进程(新派生进程)的进程ID号;在子进程中又返回一次,返回值为0。因此可以通过返回值来确定是父进程还是子进程。
fork函数在父进程和子进程中返回值不同的原因:任何子进程只有一个父进程,而且子进程总是可以通过调用getppid取得父进程的进程ID,反之,父进程有很多子进程,而且无法获取各个子进程的进程ID。如果父进程想要追踪所有子进程的进程ID,那么必须记录每次调用fork的返回值(因为在每次调用fork的时候,父进程会返回子进程的ID号,如果每次都进行记录,那么就可以追踪所有子进程的进程ID)。
父进程中调用fork之前打开的所有描述符在fork返回之后由子进程分享,我们将看到网络服务器利用这个特性:父进程调用accept之后调用fork,所接受的已连接套接字随后在父进程和子进程之间共享。通常情况下,子进程接着读写这个已连接套接字,父进程则关闭这个已连接套接字。
fork的典型用法:
(1)一个进程创建一个自身的副本,这样每个副本都可以在另一个副本执行其他任务的同时处理各自的某个操作。这是网络服务器的典型用法。
(2)一个进程想要执行另一个程序。既然创建新进程的唯一方法是调用fork,该进程于是首先调用fork创建一个自身副本,然后其中一个副本(通常称为子进程)调用exec把自身替换成新的程序,