编写一个完整的TCP客户和服务器程序所需要的基本套接口函数:
1、socket函数(客户端、服务器端都必须调用)
参数family指明协议族(family),该参数也往往被称为协议域(domain)。所以有的书上声明如下:
而且对于socket函数第一个参数,在不同书籍上可能会看到不同前缀的取值常量列表,如下两图所示:
AF_xxx与PF_xxx:
AF_前缀表地址族,PF_前缀表示协议族。历史上曾有这样的想法:单个协议族可以支持多个地址族,PF_值用来创建套接口,而AF_值用于套接口地址结构。但实际上,支持多个地址族的协议族从来就未实现过,而且头文件<sys/socket.h>中为一给定协议定义的PF_值总是与此协议的AF_值相等。
2、connect函数(TCP客户端调用)
客户在调用函数connect前不必非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。
如果是TCP套接口,调用connect函数将激发TCP的三路握手过程,而且仅在连接建立成功或出错时才返回,其中出错返回可能有以下几种情况:
(1)若TCP客户没有收到SYN分节的响应,则返回ETIMEDOUT错误。
(2)若对客户的SYN的响应是RST(表示复位),则表明该服务器主机在我们指定的端口上没有进程在等待与之连接(例如服务器进程也许没在运行)。这是一种硬错误(hard error),客户一接收到RST就马上返回ECONNREFUSED错误。
(3)若客户发出的SYN在中间的某个路由器上引发了一个“destination unreachable”(目的地不可达)ICMP错误,则认为是一种软错(soft error)。客户主机内核保存该消息,并按一定时间间隔延迟重发SYN。若在某个规定的时间(4.4BSD规定为75秒)后仍未收到响应,则 把保存的消息(即ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。
若connect失败则该套接口不再可用,必须关闭,我们不能对这样的套接口再次调用connect函数。当循环调用函数connect尝试给定主机的各个IP地址直到有一个成功时,每次connect失败后,都必须close当前的套接口描述字,重新调用socket。
3、bind函数(服务器调用、客户端可以调用也可不调用)
进程可以把一个特定的IP地址捆绑到它的套接口上,不过这个IP地址必须属于其所在主机的网络接口之一。对于TCP客户,这就为在该套接口上发送的IP数据报指派了源IP地址。对于TCP服务器,这就限定该套接口只接收那些目的地为这个IP地址的客户连接。
TCP客户通常不把IP地址捆绑到它的套接口上。当连接套接口时,内核将根据所用外出网络接口来选择源IP地址,而所用外出接口则取决于到达服务器所需的路径。
如果指定端口号为0,那么内核就在bind被调用时选择一个临时端口;
如果指定IP地址为通配地址,那么内核将等到套接口已连接(TCP)或已在套接口上发出数据报(UDP)时才选择一个本地IP地址。
对于IPv4来说,通配地址由常值INADDR_ANY来指定,其值一般为0. 它告知内核去选择IP地址。
无论是网络字节序还是主机字节序,INADDR_ANY的值(为0)都一样,因此使用htonl并非必须。不过既然头文件<netinet/in.h>中定义的所有INADDR_常值都是按照主机字节序定义的,我们应该对所有这些常值都使用htonl。
4、listen函数(TCP服务器调用)
listen函数做两件事情:
(1)当socket函数创建一个套接口时,它被假设为一个主动套接口,也就是说,它是一个将调用connect发起连接的客户套接口。listen函数把一个未连接的套接口转换成一个被动套接口,指示内核应该接受指向该套接口的连接请求。调用listen导致套接口从CLOSED状态转换到LISTEN状态。
(2)backlog参数规定了内核应该为相应套接口排队的最大连接个数。
为了理解其中的backlog参数,我们必须认识到内核为任何一个给定的监听套接口维护两个队列:
(1)未完成连接队列(incomplete connection queue),每个这样的SYN分节对应其中一项:已由客户发出并到达服务器,而服务器正在等待完成相应的TCP三路握手过程。这些套接口处于SYN_RCVD状态。
(2)已完成连接队列(completed connection queue),每个已完成TCP三路握手过程的客户对应其中一项。这些套接口处于ESTABLISHED状态。
当进程调用accept时,已完成连接队列中的队头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。
不要把backlog定义为0,因为不同的实现对此有不同的解释。如果不想让客户连接到你的监听套接口上,那就关掉该监听套接口。
5、accept函数(TCP服务器调用)
accept函数由TCP服务器调用,用于从已完成连接队列队头返回下一个已完成连接。如果已完成连接队列为空,那么进程被投入睡眠(假定套接口为缺省的阻塞方式)。
参数cliaddr和addrlen用来返回已连接的对端进程(客户)的协议地址。
如果accept成功,那么其返回值是由内核自动生成的一个全新描述字,代表与所返回客户的TCP连接。
在讨论accept函数时,我们称它的第一个参数为监听套接口(listening socket)描述字(由socket创建,随后用作bind和listen的第一个参数的描述字),称它的返回值为已连接套接口(connected socket)描述字。区分这两个套接口非常重要。一个服务器通常仅仅创建一个监听套接口,它在服务器的生命期内一直存在。内核为每个由服务器进程接受的客户连接创建一个已连接套接口(也就是说对于它的TCP三路握手过程已经完成)。当服务器完成对于某个给定客户的服务时,相应的已连接套接口就被关闭。
本函数最多返回三个值:一个既可能是新套接口描述字也可能是出错指示的整数、客户进程的协议地址(由cliaddr指针所指)以及该地址的大小(由addrlen指针所指)。如果我们对返回客户协议地址不感兴趣,那么可以把cliaddr和addrlen均设置为空指针。
6、fork和exec函数(构建并发服务器)
fork函数:。
exec函数:。
7、close函数
UNIX通常的close函数也用来关闭套接口,并终止TCP连接。
close一个TCP套接口的缺省行为是把该套接口标记成已关闭,然后立即返回到调用进程。该套接口描述字不能再由调用进程使用,也就是说它不能再作为read或write的第一个参数。然而TCP将尝试发送已排队等待发送到对端的任何数据,发送完毕后发生的是正常的TCP连接终止序列。
8、getsockname和getpeername函数
getsockname:返回与某个套接口关联的本地协议地址。
getpeername:返回与某个套接口关联的远地协议地址。
这两个函数返回与某个网络连接的两端中任何一端相关联的协议地址,对于IPv4和IPv6来说,就是IP地址和端口号的组合。
需要这两个函数的理由如下:
(1)在没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。
(2)在以端口号0调用bind(告知内核去选择本地端口号)后,getsockname用于返回由内核赋予的本地端口号。
(3)getsockname可用于获取某个套接口的地址族。
(4)在一个以通配IP地址调用bind之后的TCP服务器上,与某个客户的连接一旦建立(accept成功返回),getsockname就可以用于返回由内核赋予该连接的本地IP地址。在这样的调用中,套接口描述字必须是已连接套接口的描述字,而不是监听套接口的描述字。
(5)当一个服务器是由调用过accept的某个进程通过调用exec更换了程序时,它能够获取客户身份的唯一途径便是调用getpeername。
总结:
所有客户和服务器都从调用socket开始,它返回一个套接口描述字。客户随后调用connect,服务器则调用bind、listen和accept。套接口通常使用标准的close函数关闭。