`

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

阅读更多
    下图是一对 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 */
  • 大小: 24.8 KB
  • 大小: 14.2 KB
  • 大小: 33.1 KB
  • 大小: 18.1 KB
  • 大小: 17.5 KB
分享到:
评论

相关推荐

    linux域套接字面向连接和面向非连接的代码

    通常,代码会使用`socket()`函数创建套接字,`bind()`函数将套接字与本地地址关联,`listen()`函数设置套接字为监听模式,以及`accept()`函数接收连接请求。 2. `ipc_test`:这个名字暗示了一个用于测试域套接字...

    UDP 套接字连接

    总结来说,理解UDP套接字连接的关键在于掌握如何创建和使用UDP套接字,以及如何在无连接的情况下正确地发送和接收数据。在实际应用中,UDP常用于实时性要求高、对数据完整性和顺序性不敏感的场景,如视频流传输、...

    winsock套接字

    Winsock 套接字编程中,创建套接字使用 `socket()` 函数,该函数返回一个套接字描述符。套接字描述符是一个整数值,用于标识套接字。 `socket()` 函数的原型为: ```c SOCKET PASCAL FAR socket(int af, int type, ...

    介绍套接字编程的基本原理,简单易懂

    在这篇文章中,我们将介绍套接字编程的基本原理,包括客户机/服务器模式、基本套接字、套接字连接、数据传输、多路复用和关闭套接字等内容。 一、客户机/服务器模式 客户机/服务器模式是TCP/IP网络中两台计算机...

    套接字编程学习套接字编程学习

    总之,套接字编程是构建网络服务的基础,涉及到诸如套接字创建、选项设置、绑定、监听和接受连接等关键步骤。理解这些概念对于开发任何基于网络的应用程序都是至关重要的。在实践中,我们需要不断优化和调整这些步骤...

    套接字编程之聊天小工具

    如`WSAStartup`用于初始化套接字库,`socket`函数创建套接字,`bind`绑定本地地址,`listen`开始监听连接请求,`accept`接收连接,`send`和`recv`用于发送和接收数据,以及`closesocket`关闭套接字等。 在聊天小...

    易语言枚举套接字与发送封包

    在实际应用中,易语言提供了一系列的API函数来支持这些操作,例如`创建套接字`、`连接套接字`、`发送数据`和`关闭套接字`等。开发者可以根据需求组合这些函数,实现定制化的网络通信功能。 3. 文件"一方博客.txt...

    Socket套接字—Java套接字编程

    10. **性能优化**:优化套接字通信性能包括合理设置缓冲区大小、减少不必要的网络交互、及时关闭无用的连接等。例如,通过使用缓冲区批量读写数据,可以减少网络I/O操作的次数,提高效率。 总的来说,Java套接字...

    udp和tcp套接字简单例子

    它提供了创建、绑定、监听、接受、连接、发送和接收数据等函数,支持UDP和TCP套接字。 **UDP套接字示例** 1. 创建套接字:`socket()`函数创建一个套接字。 2. 绑定套接字:`bind()`函数将套接字与本地地址关联。 3...

    Qt套接字编程源码

    在软件开发中,网络通信是不可或缺的一部分,而Qt框架提供了一套强大且易用的套接字编程接口,使得开发者能够方便地实现客户端和服务器之间的数据交换。本篇将深入探讨Qt套接字编程的原理、使用方法以及源码解析。 ...

    网络套接字实现服务器返回客户机发送数据

    - **接受连接**:当客户端尝试连接时,服务器调用accept()函数,这会创建一个新的套接字来处理这个连接,并返回一个新的套接字句柄用于与客户端通信。 - **接收和发送数据**:使用recv()函数接收客户端发送的数据...

    tcp套接字例子windos

    实际的代码会包括上述步骤的实现,例如创建套接字、绑定、监听、接受连接、使用`select`监控套接字状态、读写数据以及最后关闭套接字。 在实际项目中,理解并熟练掌握这些TCP套接字和`select`函数的应用是构建高效...

    套接字socket编程文档

    TCP套接字提供面向连接、可靠的服务,确保数据的顺序传输和错误检查;而UDP套接字则提供无连接、不可靠的服务,具有更高的传输效率。 二、套接字API 在大多数操作系统中,如Linux、Windows,套接字API由系统提供,...

    DUP套接字客户端

    接下来,创建一个UDP套接字,使用`socket`函数指定`AF_INET`(IPv4)和`SOCK_DGRAM`(UDP)。 发送数据时,你需要构造一个`sockaddr_in`结构体,其中包含目标IP地址和端口号。然后,使用`sendto`函数将数据发送到...

    数据报套接字实现广播

    5. 关闭套接字:当通信完成后,调用close()函数关闭套接字。 "例程7(广播程序)"可能进一步扩展了这个概念,展示了如何将目的地址设置为广播地址,从而实现广播通信。广播通信的实现通常需要设置套接字选项,比如`...

    套接字客户端和服务器java代码

    两个常用的构造函数是 Socket(InetAddress addr, int port) 和 Socket(String host, int port),两个构造函数都创建了一个基于Socket的连接服务器端流套接字的流套接字。对于第一个InetAddress子类对象通过addr参数...

    基于tcp的异步套接字客户端服务端通信

    - **创建套接字**:使用`socket()`函数创建套接字。 - **连接服务器**:调用`connect()`函数与服务器建立连接。 - **异步连接**:通常通过设置套接字选项(如Windows的WSAAsyncSelect或epoll的事件监听)实现非...

    套接字通信实现多人网络聊天

    1. 初始化:客户端创建一个连接套接字,并尝试连接到服务器的IP地址和端口号。 2. 数据发送:连接成功后,客户端可以向服务器发送聊天消息。 3. 数据接收:同时,客户端也需要监听服务器发来的消息,并显示在界面上...

    套接字通信socket

    6. **关闭连接**:完成数据传输后,使用`close()`函数关闭套接字。 在实际编程中,我们还需要处理错误,例如网络中断、超时等。同时,为了提高网络效率,可以使用缓冲区来批量处理数据,避免频繁的系统调用。 在...

Global site tag (gtag.js) - Google Analytics