套接字创建、连接和关闭函数

    下图是一对 TCP 客户与服务器进程之间发生的一些典型事件的时间表。

    为执行网络 I/O,一个进程必须做的第一件事就是调用 socket 函数,指定期望的通信协议类型。
#include <sys/socket.h>
int socket(int family, int type, int protocol);
                                   /* 返回:若成功,则为非负描述符;否则为 -1 */

    其中,family 参数指明协议族,type 参数指明套接字类型,protocol 参数为某个协议类型常值,或者设为 0,以选择所给定 family 和 type 组合的系统默认值。
    下面各表分别给出了参数 family、type、protocol 及 family 和 type 的组合的值的情况。



    TCP 客户用 connect 函数来建立与 TCP 服务器的连接。
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
                           /* 返回:若成功则为 0;否则为 -1 */

    sockfd 是由 socket 函数返回的套接字描述符,第二个、第三个参数分别是一个指向包含有服务器的 IP 地址和端口号的套接字地址结构以及该结构的大小。
    客户在调用 connect 前不必非得调用 bind 函数,因为如果需要的话,内核会确定源 IP 地址,并选择一个临时端口作为源端口。如果是 TCP 套接字,调用 connect 函数将触发 TCP 的三路握手过程,而且仅在建立成功或出错时才返回。其中出错返回可能有以下几种情况。
    1、若 TCP 客户在一定的时间内没有收到 SYN 分节的响应,则返回 ETIMEDOUT 错误。
    2、若对客户的 SYN 的响应是 RST(表示复位),则表明该服务器主机在所指定的端口上没有进程在等待与之连接(例如服务器进程也许没有运行)。这是一种硬错误(hard error),客户一接收到 RST 就马上返回 ECONNREFUSED 错误。一般产生 RST 的三个条件是:目的地为某端口的 SYN 到达,然而该端口上没有正在监听的服务器;TCP 想取消一个已有连接;TCP 接收到一个根本不存在的连接上的分节。
    3、若客户发出的 SYN 在中间的某个路由器上引发了一个“目的地不可达”的 ICMP 错误,则认为是一种软错误(soft error),客户主机保存该消息,并按一定的时间间隔继续发送 SYN。若在某个规定的时间后仍未收到响应,则把保存的消息作为 EHOSTUNREACH 或 ENETUNREACH 错误返回给进程。另外,这两种情形也是有可能的:一是按照本地系统的转发表,根本没有到达远程系统的路径;二是 connect 调用根本不等待就返回。
    按照 TCP 状态转换图,connect 函数导致当前套接字从 CLOSED 状态转移到 SYN_SENT 状态。若 connect 失败则该套接字不再可用,必须关闭,不能对这样的套接字再次调用 connect 函数。当循环调用该函数为给定主机尝试各个 IP 地址直到有一个成功时,在每次 connect 失败后,都必须 close 当前的套接字描述符并重新调用 socket。

    bind 函数把一个本地协议地址赋予一个套接字。
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
                                       /* 返回值:若成功,则为 0;否则为 -1 */

    其中第二个参数是一个指向特定于协议的地址结构的指针。对于 TCP,调用 bind 函数可以指定一个端口号,或指定一个 IP 地址,或两者都指定,也可以都不指定。下表汇总了如何根据预期的结果来设置 sin_addr/sin6_addr 和 sin_port/sin6_port 的值。

    如果指定端口号为 0,内核就在 bind 被调用时选择一个临时端口,bind 并不能返回该端口值。为得到该临时端口值,只能调用函数 getsockname 来返回协议地址。
    如果指定 IP 地址为通配地址,那么内核将等到套接字已连接(TCP)或已在套接字上发出数据报(UDP)时才选择一个本地 IP 地址。让内核来选择临时端口对于 TCP 客户来说是正常的,除非需要一个预留端口,而对于 TCP 服务器来说却极为罕见,因为服务器是通过它们的众所周知端口被认识的(该规则的例外是远程过程调用(Remote Procedure Call, RPC)服务器,它们通常就由内核为它们的监听套接字选择一个临时端口,而该端口随后被 RPC 端口映射器进行注册。客户在调用 connect 这些服务器之前,必须与端口映射器联系以获取它们的临时端口,这种情况也适用于 UDP 的 RPC 服务器)。
    对于 IPv4 地址来说,通配地址由常值 INADDR_ANY 来指定,其值一般为 0,它告知内核去选择 IP 地址。一般这样使用它:
            struct sockaddr_in    servaddr;
            servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    虽然无论是网络字节序还是主机字节序,INADDR_ANY 的值都一样,可以不必使用 htonl,但既然头文件 <netinet/in.h> 中的所有“INADDR_”常值都是按照主机字节序定义的,所以为了格式统一,也推荐都使用 htonl。
    而对于 IPv6,因为 128 位的 IPv6 地址是存放在一个结构中的(C 语言中赋值语句的右边无法表示常值结构),所以一般这样来使用通配地址:
            struct sockaddr_in6    serv;
            serv.sin6_addr = in6addr_any;
    头文件 <netinet/in.h> 中含有 in6addr_any 的 extern 声明,系统预先分配 in6addr_any 变量并将其初始化为常值 IN6ADDR_ANY_INIT。
    从 bind 函数返回的一个常见错误是 EADDRINUSE(“Address already in use”)。

    listen 函数仅由 TCP 服务器调用,而且应在调用 socket 和 bind 之后,并在调用 accept 函数之前调用。
#include <sys/socket.h>
int listen(int sockfd, int backlog);   /* 返回值:若成功,则为 0,否则为 -1 */

    该函数的第二个参数规定了内核应该为相应套接字排队的最大连接个数。为理解该参数,必须认识到内核为每一个给定的监听套接字维护两个队列:
    (1) 未完成连接队列。每个这样的 SYN 分节对应其中一项:已由某个客户发出并到达服务器,而服务器正在等待完成相应的 TCP 三路握手过程。这些套接字处于 SYN_RCVD 状态。
    (2) 已完成连接队列。每个已完成 TCP 三路握手的客户对应其中一项,这些套接字处于 ESTABLISHED 状态。
    下图描绘了监听套接字的这两个队列。

    当来自客户的 SYN 到达时,TCP 就在未完成连接队列中创建一个新项(若该队列是满的,TCP 就忽略该分节),直到三路握手正常完成时,才将该项移到已完成连接队列的队尾。当进程调用 accept 时,已完成连接队列中的队头项将返回给进程,或者如果队列为空,那么进程将被投入睡眠,直到 TCP 在该队列中放入一项才唤醒它(假定为默认的阻塞套接字)。
    在三路握手完成之后,但在服务器调用 accept 之前到达的数据应由服务器 TCP 排队,最大数据量为相应已连接套接字的接收缓冲区大小。

    accept 函数也是由 TCP 服务器调用,用于从已完成连接队列头返回下一个已完成连接。
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
                                    /* 返回值:若成功则为非负描述符,否则为 -1 */

    参数 cliaddr 和 addrlen 用来返回已连接的对端进程的协议地址,如果对返回客户协议地址不感兴趣,则可以将它们均置为空指针。addrlen 是值-结果参数:调用前,先将由 addrlen 所引用的整数值置为由 cliaddr 所值的套接字地址结构的长度,返回时,该整数值即为由内核存放在该套接字地址结构内的确切字节数。如果 accept 成功,那么返回一个由内核自动生成的一个全新描述符,代表与所返回客户的 TCP 连接。相比较于监听套接字描述符而言,我们称之为已连接套接字描述符。

    套接字的关闭则是使用通常的文件描述符关闭函数 close。
#include <unistd.h>
int close(int sockfd);       /* 返回值:若成功则为 0;否则为 -1 */

    套接字关闭后,该套接字描述符就不能再由调用进程使用,TCP 在尝试发送完已排队等待发送到对端的任何数据后,就会发送正常的 TCP 连接终止序列。不过其实准确说来,close 函数只是将套接字描述符的引用计数减 1,如果描述符的引用计数仍大于 0,该 close 调用并不引发 TCP 的四分组连接终止序列(如果确实想在某个 TCP 连接上发送一个 FIN,可以调用 shutdown 函数)。对于父进程与子进程共享已连接套接字的并发服务器来说,这也正是所期望的。所以在并发服务器中,父进程中的已连接描述符是必须显示关闭的,不然只在子进程中关闭(显式或隐式)也只是使它的引用计数由 2 减到 1 而已,该描述符将在父进程中一直占据着资源,最终随着越来越多的连接建立,父进程将耗尽可用描述符。因此典型的并发服务器的程序轮廓一般是如同下面这样(为避免代码臃肿,在此没对 bind 等执行结果做判断)。
#include <unistd.h>    // fork 函数头文件

pid_t pid;
int	listenfd, connfd;
listenfd = socket(...);

/* 填充套接字地址及端口部分 */

bind(listenfd, ...);
listen(listenfd, LISTENQ);
// 这里还应该在进入循环之前调用 signal 函数来捕获 SIGCHLD 信号,以免产生僵尸进程。
// 另外,多数系统上接收到的信号可能使父进程因 EINTR 错误(被中断的系统调用)而终
// 止,所以为了移植性还应该对标准库的 signal 函数做些处理,比如设置 sigaction 结构
// 的 sa_flags 标志为 SA_RESTART 或者 SA_INTERRUPT, 以让内核能自动重启被中断的系
// 统调用,继续先前阻塞的 accept 等慢系统调用,因为适用于慢系统调用的基本规则是:
// 当阻塞于某个慢系统调用的一个进程捕获某个信号且相应信号处理函数返回时,该系统调
// 用可能返回一个 EINTR 错误。
for(;;){
	connfd = accept(listenfd, ...);
	if((pid=fork()) == 0){	// 子进程
		close(listenfd);	// 显示关闭共享的监听套接字,可省略
		/* 处理请求 */
		close(connfd);		// 显示关闭共享的已连接套接字,可省略
		exit(0);`	// 子进程退出,避免其继续往下执行
	}
	close(connfd);	// 父进程必须显示关闭已连接套接字描述符
}

    通常终止网络连接的方法是调用 close 函数,不过 close 有两个限制,而这两个限制可以使用 shutdown 函数来避免。
    (1)close 把描述符的引用计数减 1,仅在该计数变为 0 时才关闭套接字,而 shutdown 可以不管引用计数就激发 TCP 的正常连接终止序列。
    (2)close 会一次性终止读和写两个方向的数据传送,而 shutdown 可以仅关闭一半 TCP 连接。
#include <sys/socket.h>
int shutdown(int sockfd, int howto);
                         /* 返回值:若成功,返回 0;否则,返回 -1 */

    参数 howto 的值可以决定 shutdown 函数的关闭行为。
    * SHUT_RD:表示关闭连接的读这一半——套接字中不再有数据可读,而且套接字接收缓冲区中的现有数据都会被丢弃。进程不能再对这样的套接字调用任何读函数,并且由该套接字接收的来自对端的任何数据都会被确认,然后悄然丢弃。
    * SHUT_WR:关闭连接的写这一半——对于 TCP 套接字,这称为半关闭。当前留在套接字发送缓冲区中的数据将被发送掉,后跟 TCP 的正常连接终止序列。不管套接字描述符的引用计数是否为 0,这样的写半部关闭都会照样执行。进程不能再对这样的套接字调用任何写函数。
    * SHUT_RDWR:连接的读半部和写半部都关闭。

    getsockname 函数可用来获取与某个套接字关联的本地协议地址,而 getpeername 则可用来获取与某个套接字关联的外地协议地址。
#include <sys/socket.h>
int getsockname(int sockfd, struct sockaddr *localaddr, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
                               /* 返回值:若成功,都返回 0;否则,都返回 -1 */

猜你喜欢

转载自aisxyz.iteye.com/blog/2388609