Unix/Linux编程: Socket API

socket是一种IPC方法,它允许位于同一主机或者使用网络连接起来的不同主机上的应用程序之间交换数据。

关键的 socket 系统调用包括以下几种。

  • socket()系统调用创建一个新 socket。
  • bind()系统调用将一个 socket 绑定到一个地址上。通常,服务器需要使用这个调用来将其 socket 绑定到一个众所周知的地址上使得客户端能够定位到该 socket 上。
  • listen()系统调用允许一个流 socket 接受来自其他 socket 的接入连接。
  • accept()系统调用在一个监听流 socket 上接受来自一个对等应用程序的连接,并可选地返回对等 socket 的地址。
  • connect()系统调用建立与另一个 socket 之间的连接。

socket I/O 可以使用传统的 read()和 write()系统调用或使用一组 socket 特有的系统调用(如send()、recv()、sendto()以及 recvfrom())来完成。在默认情况下,这些系统调用在 I/O 操作无法被立即完成时会阻塞。通过使用 fcntl() F_SETFL 操作来启用 O_NONBLOCK 打开文件状态标记可以执行非阻塞 I/O。

在linux上可以通过调用ioctl(fd, FIONREAD, &cnt)来获取文件描述符fd引用的流socket中可用的未读字节数。对于数据报socket来讲,这个操作会返回下一个未读数据报中的字节数(如果下一个数据报的长度为零的话就返回零)或在没有未决数据报的情况下返回 0。这种特性没有在 SUSv3 中予以规定。

listen

 NAME
      listen - listen for connections on a socket

SYNOPSIS
      #include <sys/types.h>          /* See NOTES */
      #include <sys/socket.h>

     int listen(int sockfd, int backlog);

DESCRIPTION
	 listen()系统调用将文件描述符 sockfd 引用的流 socket 标记为被动。这个 socket 后面会被
	 用来接受来自其他(主动的)socket 的连接
	 
	 backlog规定了内核应该为这个套接字排队的最大连接个数

	 无法在一个已连接的 socket(即已经成功执行 connect()的 socket 或由 accept()调用返回的
	 socket)上执行 listen()。

RETURN VALUE
	 成功0, 错误-1并设置error

listen仅由TCP服务器调用,它做两件事:

  • listen函数把一个未连接的套接字转换成一个被动套接字,指示内核应接收指向该套接字的连接请求
    • 初始化创建的套接字,可以认为是一个"主动"套接字,其目的是之后主动发起请求(通过调用 connect 函数)
    • 通过 listen 函数,可以将原来的"主动"套接字转换为"被动"套接字,告诉操作系统内核:“我这个套接字是用来等待用户请求的。”当然,操作系统内核会为此做好接收用户请求的一切准备,比如完成连接队列。。
    • 调用listen之后,套接字从CLOSED状态转换为LISTEN状态
  • 将创建一个监听队列以存放待处理的客户连接
    • backlog规定了内核应该为这个套接字排队的最大连接个数,也就是说未完成连接队列的大小
    • 监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也收到ECONNREFUSED错误。
    • 也就是说,这个参数的大小决定了可以接收的并发数目。这个参数越大,并发数目理论上也越大。但是参数过大也会占用更多的系统资源

要理解 backlog 参数的用途首先需要注意到客户端可能会在服务器调用 accept()之前调用connect()。这种情况是有可能会发生的,如服务器可能正忙于处理其他客户端。这将会产生一个未决的连接,如下图所示:
在这里插入图片描述
内核必须要记录所有未决的链接请求的相关信息,这样后继的accept()就能够处理这些请求了。backlog参数允许限制这种未决连接的数量。

在内核版本2.2之前的linux中,backlog参数是指所有处于半连接状态(SYNC_RCVD)和完全连接状态(ESTABLISHED)的socket的上限。
在内核版本2.2之后,它只表示处于完全连接状态的socket的上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义。backlog参数的典型值是5

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

  • 未完成连接队列:当监听套接字收到客户端发来的SYN之后,而服务器还没有和这个连接进行TCP三次握手的时候,这些套接字处理SYN_SEND状态。
  • 已完成连接队列:每个已经完成TCP三路握手过程的客户对应其中一项。这些套接字处于ESTABLISHED状态
    在这里插入图片描述
    每当在未完成队列中创建一项时,来自监听套接字的参数就复制到即将建立的连接中,连接的创建机制是完全自动的,无需服务器插手
    在这里插入图片描述
  • 在三次握手正常完成的前提下(没有丢失分节,没有重传),未完成连接队列中的任何一项在其中的存留时间是一个RTT,其RTT的值取决于特定的客户与服务器(对于web服务器RTT一般是187ms)。
  • 当一个客户的SYNC到达时,如果这些队列是满的,TCP就暂时忽略该分节,也就是不发送RST,这样做是因为:这种情况是暂时的,客户TCP将重发SYN,期望不久就能在这些队列中找到可用空间。如果服务器TCP立即响应一个RST,客户的connect调用就会立即返回一个错误,强制应用程序处理这种情况,而不是让TCP的正常重传机制来处理。另外,客户无法区别响应SYNC的RST是“该端口没有服务器在监听”还是“该端口由服务器在监听,但是队列满了”
  • 在三次握手完成之后,服务器调用accept之前到达的数据应该由服务器TCP排队,最大数据量为相应已连接套接字的接收缓冲区大小
void Listen(int fd, int backlog)
{
    char	*ptr;

    if ( (ptr = getenv("LISTENQ")) != NULL)
        backlog = atoi(ptr);

    if (listen(fd, backlog) < 0){
        printf("listen error");
        exit(0);
    }
}

问:backlog参数对listen系统调用的实际影响

(1) 先编写服务器程序

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <memory.h>

static bool stop = false;
static void handle_term( int sig )
{
    stop = true;
}

int main( int argc, char* argv[] )
{
    struct sockaddr_in address;
    const char* ip;
    int port, backlog, sock;
    int ret;
    signal( SIGTERM, handle_term );
    
    if( argc <= 3 )
    {
        printf( "usage: %s ip_address port_number backlog\n", basename( argv[0] ) );
        return 1;
    }
    
    ip = argv[1];
    port = atoi( argv[2] );
    backlog = atoi( argv[3] );

    sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );
    
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    address.sin_port = htons( port );
    
    inet_pton( AF_INET, ip, &address.sin_addr );
    ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( sock, backlog );
    assert( ret != -1 );

    while ( ! stop )
    {
        sleep( 1 );
    }

    close( sock );
    return 0;
}

(2)命令行执行
在这里插入图片描述
并且使用telnet 192.168.0.12 12345多次建立连接,每执行一次就使用netstat -nt | grep 12345来查看状态
在这里插入图片描述

accept

accept()系统调用在文件描述符 sockfd 引用的监听流 socket 上接受一个接入连接。如果在调用 accept()时不存在未决的连接,那么调用就会阻塞直到有连接请求到达为止

  • 当客户端的连接请求到达时,服务端应答成功,连接建立,这个时候操作系统内核需要把这个事情通知到应用程序,并让应用程序感知到这个连接。这个过程,就好比电信运营商完成了一次电话连接的建立, 应答方的电话铃声响起,通知有人拨打了号码,这个时候就需要拿起电话筒开始应答。
  • accept这个函数的作用就是连接建立之后,操作系统内核和应用程序之间的桥梁
/*
* 参数:
* 	* sockfd: 监听套接字,由socket创建
* 	* cliaddr: 用来返回已连接的客户端的协议地址。如果对客户地址不管兴趣,可以置为NULL
* 	* addrlen:是值-结果参数,调用前,为cliaddr的地址长度,调用后,为内核存放在该地址结果内的确切字节数。如果对客户地址不管兴趣,可以置为NULL
* 返回值: 出错-1,成功返回已连接套接字
*/
int accept (int sockfd, struct sockaddr *__restrict cliaddr,
		   socklen_t *__restrict addr_len)
  • 函数的第一个参数 listensockfd 是套接字,可以叫它为 listen 套接字,因为这就是前面通过 bind,listen 一系列操作而得到的套接字。
  • 函数的返回值有两个部分:
    • 第一个部分cliaddr是通过指针方式获取的客户端的地址,addr_len告诉我们地址的大小。这可以理解为当我们拿起电话机时,看到了来电显示,知道了对方的毫秒
    • 第二个部分是函数的函数时,这个返回值是一个全新的描述字,代表了与客户端的连接。也叫做已连接套接字

监听套接字 VS 已连接套接字:

  • 一个服务器通常只创建一个监听套接字(listen),它在该服务器的生命期内一直存在
  • 内核会为每个服务器进程接收的客户连接创建一个已连接套接字(此时三次握手已完成–ESTABLISTEN)。当服务器完成给定客户的服务时,记得关闭这个已连接套接字

问题:为什么要把两个套接字分开呢?用一个不是挺好的么?

  • 这里和打电话的情况不一样的地方在于,打电话一旦有一个连接建立,别人是不能再打进来的,只会得到语音播报:“你拨打的电话正在通话中”。而网络程序的一个重要特性就是并发处理,不可能一个应用程序运行只会只能服务一个客户。
  • 所以监听套接字一直都存在,它是要为成千上万的客户来服务的,直到这个监听套接字关闭
  • 而一旦一个客户和服务器连接成功,完成了TCP三次握手,操作系统内核就为这个客户端生成一个已连接套接字,让应用服务器使用这个已连接套接字和客户进行通信。如果应用服务器完成了对这个客户的服务,比如一次网购下单,一次付款成功,那么关闭的就是这个已连接套接字,这样就完成了TCP连接的释放。注意,这个时候释放的只是这一个客户连接,其他被服务的客户连接可能还存在。最重要的是,监听套接字一直都处于“监听”状态,等待新的客户请求到达并服务

问: 如果监听队列中处于ESTABLISTEN状态的连接对应的客户端出现网络异常(比如掉线)或者提前退出,那么服务器对这个连接执行的accept调用是否能够成功?

(1)编写代码

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    struct sockaddr_in address;
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int sock = socket( PF_INET, SOCK_STREAM, 0 );
    assert( sock >= 0 );

    int ret = bind( sock, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

	sleep(20);  //暂停20s以便客户端连接、掉线或者退出完成

    ret = listen( sock, 5 );
    assert( ret != -1 );

    struct sockaddr_in client;
    socklen_t client_addrlength = sizeof( client );
    int connfd = accept( sock, ( struct sockaddr* )&client, &client_addrlength );
    if ( connfd < 0 )
    {
        printf( "errno is: %d\n", errno );
    }
    else
    {
        char remote[INET_ADDRSTRLEN ];
        printf( "connected with ip: %s and port: %d\n", 
            inet_ntop( AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN ), ntohs( client.sin_port ) );
        close( connfd );
    }

    close( sock );
    return 0;
}

(2)执行下面操作

  • 服务器主机运行服务端程序: ./fork_process 192.168.0.12 12345
  • 客户端主机连接服务端命令: telnet 192.168.0.12 12345 , 然后断开网络
  • 服务器主机上查看socket的状态: netstat -anp | grep 12345
    在这里插入图片描述
    在这里插入图片描述

总结: accept调用对客户端网络的断开毫不知情

(3) 执行下面操作

  • 服务器主机运行服务端程序: ./fork_process 192.168.0.12 12345
  • 客户端主机连接服务端命令: telnet 192.168.0.12 12345 , 然后关闭客户端连接
  • 服务器主机上查看socket的状态: netstat -anp | grep 12345
    在这里插入图片描述
    在这里插入图片描述

在这里插入图片描述
总结:accept只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络的变化

econnrefused

带有超时连接的accept

 socket_stream* server_socket::accept(int timeout /* = 0 */, bool* etimed /* = NULL */){
        if (etimed) {
            *etimed = false;
        }

        if (listen_fd_ == nullptr) {
            logger_error("server socket not opened!");
            return NULL;
        }

        if (timeout > 0) {
            if (listen_fd_->acl_read_wait(timeout) == -1) {
                if (etimed) {
                    *etimed = true;
                }
                return NULL;
            }
        }


        acl_socket* conn_fd = listen_fd_->acl_accept(NULL, 0, NULL);
        if (conn_fd == NULL) {
            if (open_flag_ & ACL_NON_BLOCKING) {
                logger_error("accept error %s", strerror(errno));
            } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
                logger_error("accept error %s", strerror(errno));
                return NULL;
            }
        }

        socket_stream *client = new socket_stream();
        return client;
    }

错误

 static int accept_ok_errors[] = {
                EAGAIN,
                ECONNREFUSED,
                ECONNRESET,
                EHOSTDOWN,
                EHOSTUNREACH,
                EINTR,
                ENETDOWN,
                ENETUNREACH,
                ENOTCONN,
                EWOULDBLOCK,
                ENOBUFS,			/* HPUX11 */
                ECONNABORTED,
                0,
        };

EINPROGRESS

操作正在进行中。一个阻塞的操作正在执行。

ECONNREFUSED

  • 拒绝连接。一般发生在连接建立时。
    • 拔服务器端网线测试,客户端设置keep alive时,recv较快返回0, 先收到ECONNREFUSED (Connection refused)错误码,其后都是ETIMEOUT。
  • 从connect()返回的错误,因此它只能发生在客户端(如果客户端被定义为发起连接的一方)

ENOTCONN

  • 在一个没有建立连接的socket上,进行read,write操作会返回这个错误。出错的原因是socket没有标识地址。Setsoc也可能会出错。
  • 还有一种情况就是收到对方发送过来的RST包,系统已经确认连接被断开了

套接字操作需要一个目的地址,但是套接字尚未连接.


我目前正在维护一些网络服务器软件,我需要执行大量的I/O操作。在套接字上使用时,read(),write(),close()和shutdown()调用有时可能会引发ENOTCONN错误。这个错误究竟意味着什么?什么是会触发它的条件?

  • 如果您确定自己已经正确连接,ENOTCONN最有可能是由fd在您处于请求中间的情况下(可能在另一个线程中)被关闭引起的,或者当你处于请求的中间时,通过连接丢弃。 无论如何,这意味着插座没有连接。继续并清理该套接字。它已经死了。调用close()或shutdown()就可以了。https://oomake.com/question/153611

EISCONN

  • 一般是socket客户端已经连接了,但是调用connect,会引起这个错误。
  • 在一个已经连接的套接字上调用 connect(2) 或者指定的目标地址在一个已连接的套接字上.

https://blog.csdn.net/svap1/article/details/70809798

EAGAIN

  • 表示资源不可用,使用非阻塞操作时经常遇到,并不是一种错误

如果连接数目达此上限则client 端将收到ECONNREFUSED 的错

EWOULDBLOCK

用于非阻塞模式,不需要重新读或者写

因为非阻塞,所以,read,write,等等只要不能马上操作就会返回这个错误,它的意思是让软件再尝试直到OK为止。所以,遇见这个错误只要重新再读就可以了

ENOBUFS

可用的缓冲区空间不足。只有释放了足够的资源,才能创建套接字。

ECONNRESET_

连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。

1、在客户端服务器程序中,客户端异常退出,并没有回收关闭相关的资源,服务器端会先收到ECONNRESET错误,然后收到EPIPE错误。

2、连接被远程主机关闭。有以下几种原因:远程主机停止服务,重新启动;当在执行某些操作时遇到失败,因为设置了“keep alive”选项,连接被关闭,一般与ENETRESET一起出现。

3、远程端执行了一个“hard”或者“abortive”的关闭。应用程序应该关闭socket,因为它不再可用。当执行在一个UDP socket上时,这个错误表明前一个send操作返回一个ICMP“port unreachable”信息。

4、如果client关闭连接,server端的select并不出错(不返回-1,使用select对唯一一个socket进行non- blocking检测),但是写该socket就会出错,用的是send.错误号:ECONNRESET.读(recv)socket并没有返回错误。

5、该错误被描述为“connection reset by peer”,即“对方复位连接”,这种情况一般发生在服务进程较客户进程提前终止。

ECONNABORTED Software caused connection abort

1、软件导致的连接取消。一个已经建立的连接被host方的软件取消,原因可能是数据传输超时或者是协议错误。

2、该错误被描述为“software caused connection abort”,即“软件引起的连接中止”。原因在于当服务和客户进程在完成用于 TCP 连接的“三次握手”后,客户 TCP 却发送了一个 RST (复位)分节,在服务进程看来,就在该连接已由 TCP 排队,等着服务进程调用 accept 的时候 RST 却到达了。POSIX 规定此时的 errno 值必须 ECONNABORTED。源自 Berkeley 的实现完全在内核中处理中止的连接,服务进程将永远不知道该中止的发生。服务器进程一般可以忽略该错误,直接再次调用accept。

当TCP协议接收到RST数据段,表示连接出现了某种错误,函数read将以错误返回,错误类型为ECONNERESET。并且以后所有在这个套接字上的读操作均返回错误。错误返回时返回值小于0。

https://blog.csdn.net/wuji0447/article/details/78356875

connect

服务器是通过listen调用被动接受连接,而客户端通过connect主动发起连接:

/*
* 功能: TCP客户使用connect函数来建立与TCP服务器的连接
* 返回值: 成功0,出错-1
*/
int connect (int soccket, const struct sockaddr * servaddr, socklen_t len)
  1. 如果客户端在调用connect之前没有使用bind指定IP地址和端口,内核就会自己确定源IP地址,并选择一个临时端口作为源端口
  2. 如果是TCP套接字,调用connect函数将激发TCP的三次握手工程,并且仅在连接建立成功或者出错才返回,其中出错原因可能如下:

(1)ETIMEDOUT

  • 三次连接无法建立,客户端发出的SYN包没有任何响应,返回TIMEDOUT错误
  • 这种情况比较常见的原因是对应的服务端 IP 写错。

如果第一次过6s没有收到响应,过再发一次SYN,24s之后没有响应再发一次。。。75s之后仍没有反应就返回ETIMEDOUT
当然我们可以自己定义超时时间

(2) ECONNREFUSED

  • 客户端收到了RST(复位)(这是一种硬错误(hard error))回答,这个时候客户端(connect)一收到RST就马上返回ECONNREFUSED错误
  • 这种情况比较场常见于客户端发送连接请求时的请求端口写错,因为RST是TCP在发生错误时发生的一种TCP分节。
  • 产生RST的三个条件是:
    • 目的服务器发现该端口上没有相应的进程在等待
    • TCP想取消一个已有连接
    • TCP接收到一个根本不存在的连接上的分节

在这里插入图片描述

(3)EHOSTUNREACHUNETUNREACH

  • 客户端发出SYN时在网络上引起了destination unreachable(目的地不可达)的ICMP错误。
    • 客户端主机内核会保存该消息,并按第一种情况所说的时间间隔继续发送SYN,如果在72s之后仍未收到相应,则把保存的消息(即ICMP)错误作为ehostunreachenetunreach错误返回给进程。
    • (这是一种软错误(soft error)
  • 这种情况比较常见的原因是客户端和服务器路由不通
    在这里插入图片描述

connect函数导致当前套接字从CLOSE状态转移到SYN_SENT状态,如果成功则再转移到established状态。如果connect失败将导致该套接字不可用,必须关闭,我们不能对这样的套接字再次调用connect

(4)ECONNRESET

  • 产生原因:
    • 其实这就是状态机里一个简单的竞争情形:
      • 客户端与服务端成功建立了长连接
      • 连接静默一段时间(无 HTTP 请求)
      • 服务端因为在一段时间内没有收到任何数据,主动关闭了 TCP 连接
      • 客户端在收到 TCP 关闭的信息前,发送了一个新的 HTTP 请求
      • 服务端收到请求后拒绝,客户端报错 ECONNRESET
    • 总结一下就是:服务端先于客户端关闭了 TCP,而客户端此时还未同步状态,所以存在一个错误的暂态(客户端认为 TCP 连接依然在,但实际已经销毁了)
  • 解决方案。有两种方法可选
    • 保障客户端永远先于服务端关闭TCP连接:
      • 这种方法就是把客户端的keep-alive超时时间设置得短一些(短于服务端即可)。这样就可以保证永远是客户端这边超时关闭的TCP连接,消除了错误的暂态
      • 但这样在实际的生成环境中是没法100%解决问题的,因为无论把客户端超时时间设置得多少,因为网络延时的存在,始终无法保证所有的服务端的keep-alive超时时间长于客户端的值;如果把客户端的超时时间设置得太少,又失去了意义。
    • 错误重试
      • 最佳的解决方法还是,如果出现了这种暂态导致的错误,那么重试一次请求就好,但是只识别ECONNRESET这个错误码是不够的,因为服务端可能因为某种原因真的关闭了客户端
      • 所以最佳的做法是,使用一个标记表示当前的请求是否复用了 TCP,如果错误码为 ECONNRESET 且存在标记(复用了 TCP),那么就重试一次。

其他错误: 当我们以非阻塞的方式来进行连接的时候,返回的结果如果是 -1,这并不代表这次连接发生了错误,如果它的返回结果是 EINPROGRESS,那么就代表连接还在进行中。 后面可以通过poll或者select来判断socket是否可写,如果可以写,说明连接完成了。

void Connect(int fd, const struct sockaddr *sa, socklen_t salen)
{
    if (connect(fd, sa, salen) < 0){
        printf("connect error");
        exit(0);
    }
}

close函数

#include <unistd.h>
/*
* 功能:  套接字引用计数减1.当为0时立即关闭连接
* 返回值: 成功0,出错-1
*/
 int close (int __fd)

并发服务器中父进程关闭已连接套接字只是导致相应描述符的引用计数值减1。如果引用计数值大于0,这个close调用并不引发TCP的四分组连接终止序列。一旦发现套接字引用计数到0,就会对套接字进行彻底释放,并且会TCP两个方向的数据流

如果我们确实想要在某个TCP连接上发送一个FIN(表示立即终止连接),可以改用shutdown函数。

问:套接字引用计数的含义

因为套接字可以被多个进程共享,可以理解为我们给每个套接字都设置了一个积分,如果我们通过fork的方式产生子进程,套接字就会积分+1,如果我们调用一次close函数,套接字就会积分-1。这就是套结种子引用计数的含义。

问:“TCP两个方向的数据流”怎么理解

TCP是双向的,这里说的方向,指的是数据“写入-流出”的方向。

  • 比如客户端到服务端的方向,指的是客户端通过套接字接口,向服务器端发送TCP报文;而服务端到客户端方向则是另一个传输方向。
  • 在大多数情况下,TCP连接都是先关闭一个方向,此时另外一个方向还可以正常进行数据传输,
  • 举个例子,客户端主动发起连接的中断,将自己到服务端的数据流方向关闭,此时,客户端不在往服务端写入数据,服务端读完客户端数据后就不会再有新的报文到达。但这并不意味着,TCP连接已经完全关闭,很有可能是,服务端正在对客户端的最后报文进行处理,比如去访问数据库,存入一些数据;或者是计算出某个客户端需要的值,当完成这些操作之后,服务端把结果通过套接字写给客户端,我们说这个套接字的状态此时是“半关闭”的。最后,服务端才有条不紊的关闭剩下的半个连接,结束这一段TCP连接的使命。
  • 我这里描述的,是服务器端“优雅”地关闭了连接。如果服务端处理不好,就会导致最后的关闭过程是“粗暴”的,达不到我们上面描述的“优雅”关闭的目标,形成的后果,很可能是服务端处理完信息没办法正常传送到客户端,破坏了用户侧的使用场景

问: close函数具体是如何关闭两个方向的数据流呢?

  • 在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
  • 在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个FIN报文,接下来如果再对该套接字进行写操作都会返回异常。
  • 如果对端没有检测到套接字已经关闭,还继续发送报文,就会收到一个RST报文,告诉对端:“hi,我已经关闭了,别再给我发数据了”

问:如果父进程对每个由accept返回的已连接套接字都不调用close,那么在并发服务器中将会发生什么?

  • 父进程将耗尽可用描述符,因为任何进程在任何时刻可拥有打开着的描述符数量是有限的
  • 另外,没有一个客户连接会被终止。当子进程关闭已连接套接字时,它的引用计数器将由2递减为1,又父进程没有close这个套接字,那么这个套接字的引用计数值将一直保持为1,这将妨碍TCP连接终止序列的发生,导致连接一直打开着

shutdown

#include <sys/socket.h>
/*
* 功能:
* 返回值:成功0,失败-1
*/
int shutdown(int socket, int howto);
  • sock 为需要断开的套接字
  • howto 为断开方式。

howto 在 Linux 下有以下取值:

  • SHUT_RD(0):
    • 关闭连接的“读”这个方向,应用程序不能在针对该socket文件描述符进行读操作,对该套接字进行读操作将会直接返回EOF
    • 从数据角度来看,套接字接收缓冲区已有的数据将被丢弃,如果再有新的数据流到达,会对数据进行ACK,然后悄悄的丢弃。
    • 也就是说,对端还是会接收到ACK,在这种情况下根本不知道数据已经被丢弃了
  • SHUT_WR(1)
    • 关闭连接的“写”这个方向。这就是常被称为半关闭的连接状态
    • 此时,不管套接字引用计数的值是多少,都会直接关闭连接的写方向
    • 套结字发送缓冲区已有的数据将被立即发送出去,并发送一个FIN报文给对端
    • 应用程序如果对该套接字进行写操作会报错SIGPIPE
      -SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,同时关闭套接字的读和写两个方向。

问题:使用 SHUT_RDWR 来调用 shutdown 不是和 close 基本一样吗,都是关闭连接的读和写两个方向。

其实,这两个还是有差别的

  • 第一个差别:close 会关闭连接,并释放所有连接对应的资源,而 shutdown 并不会释放掉套接字和所有的资源。
  • 第二个差别:close存在引用计数的概念,并不一定导致该套接字不可用;shutdown则不管引用计数,直接使得该套接字不可用,如果有别的进程企图使用套接字,将会受到影响。
  • 第三个差别:close的引用计数导致不一定会发出FIN结束报文,而shutdown则总会发出FIN结束报文,这在我们打算关闭连接通知对端的时候,是非常重要的。

综上:

  • close函数会关闭套接字ID,如果有其他的进程共享着这个套接字,那么它仍然是打开的,这个连接仍然可以用来读和写,只是套接字的引用计数减1。
  • shutdown会切断进程共享的套接字的所有连接,不管这个套接字的引用计数是否为零,那些试图读得进程将会接收到EOF标识,那些试图写的进程将会检测到SIGPIPE信号,同时可利用shutdown的第二个参数选择断连的方式
  • shutdown能够分别关闭socket的读/写或者全部关闭。而close只能将socket上的读全部关闭
  • 调用shutdown()只是进行了TCP断开, 并没有释放文件描述符

在大多数情况下,我们会优选 shutdown 来完成对连接一个方向的关闭,待对端处理完之后,再完成另外一个方向的关闭。

整个过程

在这里插入图片描述

在客户端发起连接请求之前,服务端必须初始化好。因此,先初始化一个socket,然后执行bind将自己的服务能力绑定到一个众所周知的地址和端口上,紧接着,服务端执行listen操作,将原来的socket转换为服务端的socket,服务端最后阻塞在accept上等待客户端请求的到来

此时,服务端已经准备期就绪。接下来是客户端,客户端必须先初始化socket,再执行connect向服务端的地址和端口发起连接请求,这里的地址和端口必须是客户端预先知晓的。这个过程,就是TCP三次握手。

一旦三次握手完成,客户端和服务端建立连接,就进入了数据传输的过程。

具体来说,客户端进程向操作系统内核发起write字节流写操作,内核协议栈将字节流通过网络设备传输到服务端,服务端从内核得到消息,将字节流从内核读入到进程中,并开始业务逻辑的处理,完成之后,服务端再将得到的结果以同样的方式写给客户端。可以看出,一旦连接建立,数据的传输就不再使单向的,而是双向的,这也是TCP的一个显著特点

当客户端完成和服务端的交互之后,比如执行一次telnet操作,或者一次http请求,需要和服务端断开连接是,就会执行close函数,操作系统内核此时会通过原先的连接链路向服务端发送一个FIN包,服务端收到之后执行被动关闭,这个时候整个链路处于半关闭状态,此后,服务端也会执行close函数,整个链路才会真正关闭。半关闭的状态下,发起close请求的一方在没有收到对方FIN包之前都认为连接是正常的;而在全关闭的状态下,双方都感知连接已经关闭。

可以看到,以上所有的操作,都是通过socket来完成的。无论是客户端的connect,还是服务端的accept,或者read/write操作等,socket是我们用来建立连接,传输数据的唯一途径

参考

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Unix/Linux系统编程是指使用C语言编写程序,能够调用操作系统提供的系统调用和库函数来完成系统级任务的程序设计过程。Unix/Linux系统编程的目的是编写高效、可靠、安全、移植性好的应用程序或系统程序。 Unix/Linux系统编程的核心代码包括使用系统调用,文件操作(读写文件、目录操作等),进程控制(fork、exec等),信号处理,网络编程等。 在Unix/Linux中,系统调用是与内核进行通讯的标准方式。程序中使用系统调用来请求内核完成某个任务。例如,open()系统调用用于打开一个文件,并返回文件描述符。read()和write()系统调用用于读写文件。 文件操作是Unix/Linux系统编程中的一个重要部分。文件操作包括打开文件、读写文件、删除文件、重命名文件等操作。另外还有目录操作,如创建目录、删除目录、遍历目录等。 进程控制是Unix/Linux系统编程中最为复杂的部分之一。进程控制包括创建新进程、执行新进程、等待进程结束、发送信号给进程等等。其中最常见的系统调用是fork()和exec()。fork()用于创建新进程,而在创建新进程之后,exec()则用于在新进程中执行新的程序。 信号处理是Unix/Linux系统编程中的一个重要概念。信号是由系统发出的一个异步事件,可以从进程内部或外部发出。进程可以对信号进行相应操作。常见的信号包括SIGINT(Ctrl+C中断信号)、SIGTERM(终止进程信号)和SIGKILL(强制终止进程信号)。 网络编程Unix/Linux系统编程中的另一个重要部分。Unix/Linux提供了许多网络编程API,例如socket()、bind()、listen()和accept()等。使用这些API可以编写服务器端和客户端程序,进行网络通信。 总之,Unix/Linux系统编程涉及到许多重要的概念和操作,涉及到操作系统底层的各种操作。因此,需要开发人员有扎实的C编程能力、熟悉Unix/Linux系统调用和库函数、了解进程控制和信号处理的概念、熟悉网络编程API以及充分了解操作系统内部的机制。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值