UNIX网络编程(四)---基本TCP套接字编程

 

UNIX网络编程(四)---基本TCP套接字编程

 

 

一、socket函数

 

 

family:

 

type:

 

protocol:

 

 

为什么还需要第三个protocol参数?协议族信息和套接字数据传输方式不足以决定采用的协议吗?为什么还需要第三个参数?

因为在同一协议族中,可能存在着多个数据传输方式相同的协议。

 

返回值:成功时返回一个非负整数值,它与文件描述符类似(),我们把它称为套接字描述符。简称sockfd。

 

二、connect函数

客户在调用函数connect前,没必要非得调用bind函数,因为如果需要的话,内核会确定源IP地址,并选择一个临时端口作为源端口。

      如果是TCP套接字,调用connect函数将激发TCP的三路握手过程,而且在连接成功或出错时才返回,其中出错有以下几种情况:

1)若TCP客户没有收到SYN分节的响应,则返回ETIMEOUT错误。

2)若对客户的SYN的响应是RST,则表明服务器主机在我们指定的端口没有进程在等待与之连接。这是一种硬错误,客户收到RST马上返回ECONNREFUSED错误。

RST产生的三个原因:1.目的地为某端口的SYN到达,然而该端口上没有正在监听的服务器;2.TCP想取消一个已有连接;3.TCP接收到一个根本不存在的连接上的分节(???)

3)若客户发出的SYN在中间某个路由器上引发了一个“destination unreachable” ICMP错误,则认为这是一种软错误。客户内核保存该信息,在规定的时间内(75s)仍然未收到相应,则把保存的信息(ICMP错误)作为EHOSTUNREACH或ENETUNREACH错误返回给进程。

 

若connect失败则该套接字不再可用,必须关闭,我们不能对这样的套接字再次调用connect函数,当循环调用函数connect为给定主机尝试各个ip地址直到有一个成功时,在每次connect失败后,都必须close当前的套接字描述符并从新调用socket.

 

 

 

三、bind函数

      bind函数把本地协议地址赋予一个套接字。

 

第二个参数是一个指向用于特定地址结构的指针,第三个参数是这个结构的长度,调用bind函数可以指定一个端口号,或者一个IP地址,或者两者都指定、都不指定。不指定的时候内核来搞定:

 

 

如果让内核啦为套接字选择一个临时端口号,函数bind并不返回所选择的值。由于bind函数的第二个参数有const限定词,它无法返回所选之值。为了得到内核所选择的这个临时端口,必须调用函数getsockname来返回协议地址。

 

 

 

四、listen函数

listen函数仅有TCP服务器调用,它做两件事情:

1)当socket函数创建一个套接字时,它被假设成为一个主动套接字,也就是发起连接的客户套接字。listen函数把一个未连接的套及诶转换成为一个被动套接字,指示内核应接受指向该套接字的连接状态。从CLOSED状态转到LISTEN状态。

2)本函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。

 

内核给任何一个监听套接字维护两个队列:

1.未完成连接队列。每个这样的SYN分节对应其中一项,已由某个客户发送并到达服务器,而服务器正在等待完成相应的TCP三路握手,这些套接字处于SYN_RECVD状态。

2.已完成连接队列。每个已完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态,也就是数据传送状态。

 

 

 

 

      当来自客户的SYN到达时,TCP在未完成连接队列中创建一个新项,然后响应以三路握手的第二个分节:服务器的SYN响应,其中捎带对客户的SYN的ACK。这一项一直保留在未完成链接队列中,直到三路握手的第三个分节(客户对服务器SYN的ACK)到达或者该项超时为止。如果三路握手正常完成,该项就从未完成连接队列中的对头项将返回给进程,或者如果该队列为空,那么进程将被投入睡眠,直到TCP在该队列中放入一项才唤醒它。在三路握手正常完成的前提下(没有丢失分节,从而没有重传),未完成连接队列中的任何一项在其中的存留时间就是一个RTT,而RTT的值取决于特定的客户与服务器。对于一个WEB服务器,许多客户与单个服务器之间的中值RTT为187ms。

       关于这两个队列需要特别注意的一点:当一个客户SYN到达时,若这些队列是满的,TCP就忽略该分节,也就是不发送RST。这么做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久能在这些队列中找到可用空间。要是服务器TCP立即响应以一个RST。客户的connect调用就会立即返回一个错误,强制应用进程处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区别响应SYN的RST究竟意味着“该端口没有服务器在监听”,还是意味着“该端口有服务器在监听,不过它的队列满了”。

        在三路握手完成之后,但在服务器调用accept之前到达的数据应由服务器TCP排队,最大数据量为相应已连接套接字的接受缓冲区大小。

 

 

五、accept函数

      

            accept函数由TCP服务器调用,用于从已完成连接队列的对头返回下一个已完成连接。如果已完成队列为空,那么进程将被投入睡眠。

 

第二个参数和第三个参数用来返回已连接的对端进程(客户)的协议地址。值-结果参数

如果accept成功,那么返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接。它的第一个参数是监听套接字描述符(由socket创建,随后被用作bind,listen的第一个参数的描述符),称它的返回值为已连接套接字描述符

区分:

一个服务器通常只创建一个监听套接字,它在服务器的生命期内一直存在。内核为每个服务器进程接受的客户连接创建一个已连接套接字(握手完成后),当服务器完成对某个给定的客户的服务时,相应的已连接套接字就被关闭。

如果对客户的身份不感兴趣,把它的第二个第三个参数置为空指针。

 

 

 

 

六、fork和exec函数

 

 

 

1)  关于exec函数(详见P90)

关于exec函数有六个,这六个exec函数之间的区别在于:待执行的程序文件是由文件名还是由路径名指定;新程序的参数是一一列出还是由一个指针数组来引用;把调用进程的环境传递给新程序还是给新程序指定新的环境。

 

 

 

七、并发服务器

 

 

        当一个连接建立,accept返回,服务器接着调用fork,然后由子进程服务客户(通过已连接套接字connfd),父进程则等待另一个连接(通过监听套接字listenfd)。既然新的客户由子进程提供服务,父进程就关闭已连接套接字。子进程在做完操作以后,显示地关闭已连接套接字。这一点并非必须,因为下一个语句就是调用exit,而进程终止处理的部分工作就是关闭所所有由内核打开的描述符。

        同时这里还牵涉到引用计数的问题,在父进程中可以关闭了连接套接字,为何在子进程中仍然可以使用呢?引用计数是当前打开着的引用该文件或套接字的描述符个数。比如socket返回后和listenfd关联的文件表项的引用计数值为1,accept返回后与connfd关联的文件表项的引用计数为1,通过fork以后,这两个描述符就在父进程与子进程之间共享,因此与这两个套接字相关联的文件表项各自的访问计数均为2,当父进程关闭connfd时,它只是把相应的引用计数值从2减为1,该套接字真正的清理和资源释放在其引用计数达到0时才发生。

 

 

 

 

八、close函数

这个函数的默认行为是把该套接字标记成已关闭,然后立即返回到调用进程。

如果父进程对每个由accept返回的已连接套接字不调用close。那么,父进程最终将耗尽可用描述符,更重要的是,没有一个客户连接会被终止。当子进程关闭已连接套接字时,它的引用计数值将由2减为1并保持不变。这将妨碍TCP链接终止序列的发送,导致连接一直打开着。

 

 

 

 

九、getsockname 和 getpeername函数

 


 

       函数的作用:
1)  在一个没有调用bind的TCP客户上,connect成功返回后,getsockname用于返回由内核赋予该连接的本地IP地址和本地端口号。

2)  当一个服务器调用过accept的某个进程通过调用exec执行程序时,它能够获取客户身份的唯一途径是调用getpeername。(P95)