Linux下Socket编程之TCP原理

一。Socket异常信息

之所以把对异常信息的介绍放到原理之前讲,是因为由于socket本身的复杂性,导致了产生各种异常的复杂性。我们应该时刻铭记的是,sokcet本身属于系统(OS),是系统对TCP/IP的实现,也就是说,socket发出的异常信息不代表程序出错,甚至不代表系统出错,而仅仅就是代表socket本身的各种异常情况。另外一点我觉得应该强调的是:socket不是TCP/IP;TCP/IP也不是socket。socket是为广泛的协议设计的,涉及TCP/IP的内容只是socket体系中一个很小的子集;而TCP/IP就更加独立于sokcet而存在——TCP/IP是协议描述;socket是对协议理论的一种实现形式。
因为socket是属于系统的,所以不同的系统对于socket有着大同小异的解释,出错描述也不尽相同。在Linux中,socket的异常信息可以通过errno获得(int类型),然后可以通过函数strerror()将int转换成字符串描述;也可以通过函数perror()直接获得其描述。
要使用errno需要包含头文件<errno.h>。我建议使用errno获得int类的错误信息的一个重要原因在于,socket的异常不一定就必然导致程序终止。Bjarne Stroustrup在介绍C++异常机制的时候对C风格的异常机制有着这样的描述:(C++对于异常)的默认响应方式是终止程序。传统的反应(对于发生异常的时候)则是装糊涂,接着做下去,以期得到最好的结果(《C++程序设计语言》第14章 异常处理)。不过以我目前的水平看来,终止正在进行的程序然后再通过异常机制重新启动一个新的流程,其代价远远大于“装糊涂”的让程序继续运行下去,只要错误不是致命的,通过简单的判断和处理或许效果更佳。
例如,socket中就有一个很有代表性的情况,在TCP连接中,如果一方意外退出——也就是说没有通过TCP退出流程退出,比如没有运行完程序关闭掉socket而直接X掉或者Ctrl+c了。socket往往会因为recv()返回值小于0而抛出一个异常。正常断开连接的时候,recv()会通过返回0表示连接已经断开,但是大多数时候,我们并不希望因为异常的断开就导致另外一端的程序终止(想象一下如果你关掉QQ腾讯的服务器程序就终止是什么概念……),所以我们必须处理这种情况。
在Linux中,远程连接异常断开(被重置)的errno代码是104,类似的,我们应该保证出现这种异常的时候程序可以继续运行。

// Filename: SockClass.hpp

#ifndef SOCK_CLASS_HPP
#define  SOCK_CLASS_HPP

#include 
< unistd.h >
#include 
< iostream >
#include 
< sys / socket.h >
#include 
< arpa / inet.h >
#include 
< errno.h >

namespace  sockClass
{
void  error_info( const   char *  s);
}

以上是头文件中的声明,下面是函数,我们这里仅仅演示处理了104错误。

namespace  sockClass
{
void  error_info( const   char *  s)
{
    
int  err_info  =  errno;
    std::cerr 
<<  strerror(err_info)  <<   " : errno:  "   <<  err_info  <<  std::endl;
    
if  (err_info  ==   104 ){
        
return ;
    }
    exit(
1 );
}
}

在windows中,错误代码由WSAGetLastError()获得,而无需设置errno。

// Filename: SockClass.hpp

#ifndef SOCK_CLASS_HPP
#define  SOCK_CLASS_HPP

#include 
< iostream >
#include 
< winsock2.h >

namespace  sockClass
{
void  error_info( const   char *  s);
}

WinSock的错误代码跟Linux中的不一样,同样的异常,WinSock的错误代码是10054。
并且,由于没有errno也就无从调用strerror(),我们最好自己写出详细的异常信息。
WinSock的详细代码信息在这里:
http://msdn.microsoft.com/en-us/library/ms740668(v=VS.85).aspx
win32下的演示代码如下:

namespace  sockClass
{
void  error_info( const   char *  s)
{
    
int  winsock_err  =  WSAGetLastError();
    perror(s);
    std::cerr 
<<   " WinSock Error:  "   <<  winsock_err  <<  std::endl;
    
if  (winsock_err  ==  WSAECONNRESET) {
        std::cerr 
<<   " Connection reset by peer. "   <<  std::endl;
        
return ;
    }
    exit(
1 );
}
}
二。实际TCP Socket类
我们在第1节中讲过,socket是一个int的文件描述符(WinSock中直接是一种抽象的描述符),我们通过对这个描述符发出指令操作socket。这是C语言的思想,在面向对象的思想中,最好socket本身是一种对象,各种方法由对象本身发出。用面向对象的思想封装socket并不困难,而且,对于描述socket的概念可能更加直观,这一节,我们边介绍socket和TCP的概念边对socket进行OO封装。
首先,每一个socket对象都具有唯一的socket文件描述符,这样可以很好的对应socket的概念。所以我们构建一个基类,并让其成为纯虚函数——这是因为socket文件描述符必须在具体的构造中才能出现,然后仍然保留一个返回原始的socket文件描述符的接口,这是为了不方便归结到类函数中的函数所预留准备的,比如极其重要的select()我们会在后面讲到,所谓有备无患。
class  BaseSock{
protected :
    
int  sockFD;
public :
    BaseSock();
    
virtual   ~ BaseSock()  =   0 ;
    
const   int &  showSockFD()  const ;
};
函数实现:
// class BaseSock

BaseSock::BaseSock():
sockFD(
- 1 )
{}

BaseSock::
~ BaseSock()
{}

const   int &  BaseSock::showSockFD()  const
{
    
return  sockFD;
}
我们把sockFD的初始值设置为-1,表明在没有派生类构造的时候这是一个非法的文件描述符号(File Descriptor)。
接下来,我们简单回顾一下第一节对于TCP Server的建立:
首先,我们需要建立一个监听socket,然后激活其监听;
然后,在client端连接信息过来之后,通过监听端口将客户端的信息传递给新的socket,从而建立通讯socket。
我们先构建listen socket:
class  TCPListenSock:  public  BaseSock{
private :
    sockaddr_in listenSockAddr;
public :
    
explicit  TCPListenSock(unsigned  short  listen_port);
    
~ TCPListenSock();
    
void  TCPListen(
        
int  max_connection_requests  =   10 const ;
};
TCPListenSock建立的目的的就是被动的等待client端寻找握手的connect(),从而收集client端的sock地址信息(包含了IP地址和端口号),然后在需要的时候传递给新的socket建立通讯socket。
TCPListenSock::TCPListenSock(unsigned  short  listen_port)
{
    sockFD 
=  socket(PF_INET,
                    SOCK_STREAM,
                    IPPROTO_TCP);
    
if  (sockFD  <   0 ) {
        sockClass::error_info(
" socket() failed. " );
    }
    memset(
& listenSockAddr,  0 sizeof (listenSockAddr));
    listenSockAddr.sin_family 
=  AF_INET;
    listenSockAddr.sin_addr.s_addr 
=  htonl(INADDR_ANY);
    listenSockAddr.sin_port 
=  htons(listen_port);
    
if  (bind(    sockFD,
                (sockaddr
* ) & listenSockAddr,
                
sizeof (listenSockAddr))  <   0 ) {
        sockClass::error_info(
" bind() failed. " );
    }
}

TCPListenSock::
~ TCPListenSock()
{
    close(sockFD);
}
TCPListenSock通过调用socket()建立sockFD;通过指定端口好指明监听端口,这是为客户端能够找到这个端口所必须的。而IP地址设置为INADDR_ANY,其实就是0,这意味着可以是任何一个server端所拥有的IP。TCPListenSock通过bind()将sockFD和SockAddr绑定在一起。这个sockFD只有本机的SockAddr意味着:1、无法建立连接,只有接受数据报;2、只能接受信息,因为没有远程目的地的SockAddr而无法发出信息。
而这对于TPC建立连接的过程来说,既是足够的,也是必须的。事实上,client端发出的第一个握手数据报就被这个sockFD所接收,而返回给client的握手应答和对client的握手请求则由新的sockFD发出。
listen()是将TCPListenSock激活为监听状态,如果不激活,那么任何握手的连接请求都将被这个sockFD所忽略。
void  TCPListenSock::TCPListen(
                        
int  max_connection_requests)  const
{
    
if  (listen(    sockFD,
                max_connection_requests) 
<   0 ) {
        sockClass::error_info(
" listen() failed. " );
    }
}
这个函数看来似乎有些多此一举,因为这个监听是可以整合到构造函数中的,也就是说,我们可以一旦建立TCPListenSock就令其激活,事实上这正是SDL_net中的做法,也是让我感到不严谨的地方,因为监听本身是socket的一个概念。
当激活监听的TCPListenSock等待远程client的connect()握手请求的时候,是调用了accept()并且产生阻塞(默认情况下),如果accept()成功返回意味着conect()握手请求请求成功,这时候就通过accept()产生了一个新的sockFD用于TCP通讯。我们把这个新的sockFD构建为TCPServerSock类:
class  TCPServerSock:  public  BaseSock{
private :
    sockaddr_in clientSockAddr;
protected :
    
char *  preBuffer;
    
int  preBufferSize;
    mutable 
int  preReceivedLength;
public :
    
explicit  TCPServerSock(
        
const  TCPListenSock &  listen_sock,
        
int  pre_buffer_size  =   32 );
    
virtual   ~ TCPServerSock();
    
int  TCPReceive()  const ;
    
int  TCPSend( const   char *  send_data,
            
const   int &  data_length)  const ;
};
这里,我们为TCPServerSock预留一个缓存,这个缓存并不是必须的,但是设置这样一个缓存至少有两个好处:
1、可以在使用时不必专门为recv()建立缓存;
2、类方法TCPReceive()和TCPSend()可以共享这个缓存,在处理很多问题时候很方便,比如echo,就不需要先把recv()的缓存读出来再由send()来发送。
将缓存已用长度preReceiveLength加上关键字mutable表示我们不关心这个长度会被更改,我们只在乎有一个缓存可以用,但是实际用了多少不重要,这样我们就可以为接受和发送的类方法加上const。
我们回到TCPServerSock的建立,TCPServerSock通过TCPListenSock accept()一个远程的client connect()握手请求而建立,所以,TCPServerSock的构造在默认情况下是阻塞的。
TCPServerSock::TCPServerSock(
                
const  TCPListenSock &  listen_sock,
                
int  pre_buffer_size):
preBufferSize(pre_buffer_size),
preReceivedLength(
0 )
{
    preBuffer 
=   new   char [preBufferSize];

    socklen_t
 clientSockAddrLen  =   sizeof (clientSockAddr);
    sockFD 
=  accept(    listen_sock.showSockFD(),
                        (sockaddr
* ) & clientSockAddr,
                        
& clientSockAddrLen);
    
if  (sockFD  <   0 ) {
        sockClass::error_info(
" accept() failed. " );
    }
    std::cout    
<<   " Client (IP:  "
                
<<  inet_ntoa(clientSockAddr.sin_addr)
                
<<   " ) conneted. "   <<  std::endl;
}

TCPServerSock::
~ TCPServerSock()
{
    delete [] preBuffer;
    close(sockFD);
}
这里需要注意一个Linux和Windows下的不同:
对于sockaddr_in(也包括sockaddr)的大小,被accept()指定的时候,Linux中用的是socklen_t,其实这就是size_t,也就是unsigned int。而WinSock中却用的是int。因为在编译中不会自动转换,所以会提示错误。
再次强调,TCPServerSock的sockFD是通过accept()建立的而不是socket(),这也是唯一一个不用socket()建立的sockFD(包括UDP的)。在client发出的connect()握手请求的数据报中,同时包含着client端的地址信息(IP地址和端口)和server端的地址信息(IP地址和端口),正是这个握手请求数据报中的两边的地址信息通过accept()被传递到TCPServerSock的sockFD中。请注意,server端的信息并非由TCPListenSock提供,因为TCPListenSock中listenSockAddr的IP地址为空(INADDR_ANY == 0),而TCPServerSock中server端的SockAddr却是具体的,由客户端的握手协议传来的(但是没有具体的体现出来)。只有具体的地址(IP地址和端口)才能提供IP数据包的目的地方向。而端口号,则因为client事先知道监听端口号,从而在握手请求中包含,最终传递给TCPListenSock中server端的SockAddr,虽然这个过程决定了这个端口号等于监听端口号,但是需要明白的是,这个端口号来自握手请求的数据报而不是TCPListenSock的listenSockAddr。
新的sockFD具有来向(本机)和去向(远程)的信息,所以可以收发数据。TCPServerSock的sockFD一旦建立,马上向远程返回一个数据报,这个数据报有两层意义:
1、表示server已经接收了client的握手请求;
2、对client发出与server这个新sockFD握手的请求。
这就是所谓第二次握手,并且也是以数据报的形式传送的。我们说过,TCP协议的目标是建立“可靠”的数据流形式的通讯,在这个数据流的通道建立起来以前,只能采用数据报的形式传送数据。
在另外一边的客户端,我们分析一下TCPClientSock的建立过程。
class  TCPClientSock:  public  BaseSock{
private :
    sockaddr_in serverSockAddr;
protected :
    
char *  preBuffer;
    
int  preBufferSize;
    mutable 
int  preReceivedLength;
public :
    TCPClientSock(
        
const   char *  server_IP,
        unsigned 
short  server_port,
        
int  pre_buffer_size  =   32 );
    
virtual   ~ TCPClientSock();
    
int  TCPReceive()  const ;
    
int  TCPSend( const   char *  send_data,
            
const   int &  data_length)  const ;
};
我们看到TCPClientSock的类与TCPServerSock很类似,构造函数的差别是,TCPClientSock需要提供server端的IP地址和端口号。
TCPClientSock::TCPClientSock(
                    
const   char   * server_IP,
                    unsigned 
short  server_port,
                    
int  pre_buffer_size):
preBufferSize(pre_buffer_size),
preReceivedLength(
0 )
{
    preBuffer 
=   new   char [preBufferSize];

    sockFD 
=  socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
    
if  (sockFD  <   0 ) {
        sockClass::error_info(
" sock() failed. " );
    }

    memset(
& serverSockAddr,  0 sizeof (serverSockAddr));
    serverSockAddr.sin_family 
=  AF_INET;
    serverSockAddr.sin_addr.s_addr 
=  inet_addr(server_IP);
    serverSockAddr.sin_port 
=  htons(server_port);

    
if  (connect(sockFD,
                (
struct  sockaddr * ) & serverSockAddr,
                
sizeof (serverSockAddr))  <   0  ) {
        sockClass::error_info(
" connect() failed. " );
    }
}

TCPClientSock::
~ TCPClientSock()
{
    delete [] preBuffer;
    close(sockFD);
}
TCPClientSock通过socket()建立起sockFD,然后指定服务器的serverSockAddr,然后通过connect()向serverSockAddr指定的服务器发出握手请求。需要说明的是,调用connect()的时候,系统会检查TCPClientSock的sockFD是否已经绑定了本机的SockAddr,事实上我们也可以通过bind()将本机的IP和指定的端口号绑定在这个sockFD上,但是我们并不关心这个IP地址和端口号(况且很多主机并没有公网IP,特别在中国),所以通常我们不自己去绑定,这样系统就会帮我们完成绑定工作,分配一个空闲的端口号作为本机地址的端口号。
这样TCPClientSock具有来向(本机地址,通常由系统自动完成绑定,也可以指定)和去向(指定的server端地址)的地址信息,所以可以收发信息。于是,TCPClientSock发出的第一个数据报是发给server监听socket的握手请求数据报,TCPListenSock接收这个数据报后,将相关信息传递给TCPServerSock建立新的sockFD,我们上一节讲到,这个新的sockFD建立起来之后马上就向client端返回一个数据报:一方面表示接受第一次握手请求,另外一方面发出第二次握手请求。
收到第二次握手请求后,connect()才会返回,不然就会阻塞,非常“尽力”的去连接server。这个“尽力”的程度跟系统有关,在我的试验中,windows下很快,就几秒;而Debian则接近6分钟!
connect()返回的同时,向server发出了第三次握手的信息,这个信息是对第二次握手请求的认可。所以,第一次和第二次握手包含着连接的请求;而第二次和第三次握手则包含着对握手请求的认可,他们都是在告诉对方:我知道并同意你连接上我了。
至此,TCP三次握手的概念在socket中完整的实现,建立起数据流的TCP通信通道。
三。TCP三次握手
前面3个小节介绍了socket机制对TCP协议三次握手的实现,需要强调的是,与协议独立于实现类似,TCP的三次握手是独立于socket体系的理论。在TCP协议中,三次握手是通过3个TCP格式的IP数据报来实现的。TCP格式的IP数据报中包含着TCP首部,TCP首部信息中包含着对每一个数据报具体内容的描述。我们这里需要介绍的首部位(bit)标志只有3个:
SYN:同步序号用来发起一个连接。因为TCP协议要求数据传送是可靠的,他的实现方式就是对传输的数据的每一个字节(byte)按顺序编号。但是初始序列号(ISN:Initial Sequence Number)并非从0开始,而是一个随时间周而复始变化的32位无符号整数。当一方发起连接的时候,SYN就会被设置成1,同时,在发送的数据部分用一个字节来表明这是一个新连接的开始。因此,假设发起连接的一方的ISN为n,因为SYN会在数据部分添加一个字节表示这是一个新连接的开始,所以这时候的字节序号就成了n+1。
ACK:确认序号有效。TCP协议要求自动检验数据的可靠性,实现方式就是检验字节序号是否正确的衔接。假如接收数据的一方序号已经是m,那么其返回给发送方确认有效的序号就是m+1。一旦连接,ACK始终设置为1,即表示序号有效,并且在所有数据包中总是存在。但是数据是否真的被TCP采用要看序号是否能对应。如果发送方传来的字节序号没有从m+1开始,那么这个IP数据包就不会被采用,返回ACK信息序号依然是m+1;如果发送方传来的字节序号尽管是从m+1开始的,但是在效验时发生了错误,这个数据报依然不会被采用,返回的ACK信息序号依然是m+1。直到接收了通过TCP检验的数据,序号才会继续增加,例如,传来的数据字节序号从m+1开始到m+k结束,并且通过了TCP效验,那么再次传回的ACK信息,序号就成为了m+k+1。
FIN:发送端完成发送。与SYN类似,FIN也会在数据部分占用一个字节,表示这是一个结束符号。
TCP的三次握手过程如下:
1、第一个SYN连接请求由客户端发起,这个数据报将SYN设置为1表示是一个连接请求,并且包含着这次连接的ISN,我们假设其值为n。
2、服务器端收到第一次握手请求的数据报后开始构建反馈的数据报。反馈数据报包括两个部分:第一部分是将连接请求的序号反馈回去,因为SYN本身占了一个字节,所以反馈回去的序号就是n+1;第二部分是自己也向客户端发起SYN连接请求,也将SYN设置为1,并包含这个新连接的ISN,我们设其值为m。
3、客户端回应服务器端的SYN连接请求,将服务器端到客户端连接的序号反馈回去,因为SYN占了一个字节,所以反馈给服务器端的序号是m+1。
由此,我们可以看到,TCP中,客户端到服务器端,服务器端到客户端的连接是分别建立的,具有不同的ISN(n和m),我们在后面可以看到,这也就意味着这两个连接在正常情况下需要分别的断开。
六。字节流的发送与接收
从TCP三次握手的原理我们可以看到,TCP有“保障”的连接实际上可以看做是两个单向的连接:一个通道只负责发送,另外一个只负责接收。并且,传送的信息是以字节为单位保证顺序的。
在socket机制中,应用层的程序以send()函数将数据首先发送到本机系统的发送缓存中,我们称之为SendQ,意指这是一个FIFO(先进先出)的队列。这个缓存是系统决定的,并不是在我们的程序中指定的。然后socket机制负责将SendQ中的数据以字节为单位,按照顺序发送给对方的接收缓存RecvQ中。RecvQ也是一个属于系统的FIFO缓存队列。从程序员的角度看,send()函数只负责把数据送入SendQ,而SendQ何时将数据发送则是不可控的。所以,send()通常不会阻塞,只有在不能立即将数据发送给SendQ的时候才会阻塞,这往往是因为SendQ缓存已满。另外,SendQ并不负责统计每次send()所发送来的字节流的长度,事实上这个长度在TCP中没有意义,因为所有数据都以字节为单位按照FIFO的形式排列在队列中,而并不在乎来自于哪一次的send()。这也就是所谓的TCP无边缘保证,TCP的send()并不在乎每次传送的数据有多少,而只是致力于将数据以字节为单位按照FIFO的形式排列在SendQ队列中。我们看一下TCPServerSock和TCPClientSock的TCPSend()方法:
int  TCPServerSock::TCPSend( const   char *  send_data,
                           
const   int &  data_length)  const
{
    
if  (data_length  >  preBufferSize) {
        sockClass::error_info(
" Data is too large, resize preBufferSize. " );
    }

    
int  sent_length  =  send(    sockFD,
                            send_data,
                            data_length,
                            
0 );
    
if  (sent_length  <   0 ) {
        sockClass::error_info(
" send() failed. " );
    } 
else   if  (sent_length  !=  data_length) {
        sockClass::error_info(
" sent unexpected number of bytes. " );
    }

    
return  sent_length;
}
int  TCPClientSock::TCPSend( const   char *  send_data,
                           
const   int &  data_length)  const
{
    
if  (data_length  >  preBufferSize) {
        sockClass::error_info(
" Data is too large, resize preBufferSize. " );
    }

    
int  sent_length  =  send(    sockFD,
                            send_data,
                            data_length,
                            
0 );
    
if  (sent_length  <   0 ) {
        sockClass::error_info(
" send() failed. " );
    } 
else   if  (sent_length  !=  data_length) {
        sockClass::error_info(
" sent unexpected number of bytes. " );
    }

    
return  sent_length;
}
可以看到,这两个方法除了分属于不同的类名字不一样,其他都是一样的。send()的返回值是实际发送的字节长度。
在收信息的另外一边,当RecvQ没有数据时,recv()就会阻塞(默认情况下),每当有数据可接收,recv()就会返回实际接收到的数据长度。recv()同样不在乎每次接收的数据有多少,其参数只有一个最大长度限制,这个限制是应用程序分配给每次recv()储存数据的缓存大小。所以TCP的send()和recv()不是一一对应的:send()只负责将数据写入本机的SendQ,而recv()只负责把本机RecvQ中的数据读出来。假设send()传送了m+n字节,但是第一次到达远程目的地的RecvQ中只有m字节,于是这里的recv()就会马上返回m字节;剩下的n字节第二次才姗姗来迟,那么就需要第二次调用recv()来接收。
int  TCPServerSock::TCPReceive()  const
{
    preReceivedLength 
=  recv(    sockFD,
                                preBuffer,
                                preBufferSize,
                                
0 );
    
if  (preReceivedLength  <   0 ) {
        sockClass::error_info(
" recv() failed. " );
    } 
else   if  (preReceivedLength  ==   0 ) {
        std::cout 
<<   " Client has been disconnected./n " ;
        
return   0 ;
    }
    
return  preReceivedLength;
}
int  TCPClientSock::TCPReceive()  const
{
    preReceivedLength 
=  recv(    sockFD,
                                preBuffer,
                                preBufferSize,
                                
0 );
    
if  (preReceivedLength  <   0 ) {
        sockClass::error_info(
" recv() failed. " );
    } 
else   if  (preReceivedLength  ==   0 ) {
        std::cout 
<<   " Disconnected from server./n " ;
        
return   0 ;
    }
    
return  preReceivedLength;
}
可以看到这2个方法也几乎是一模一样——除了名字和对异常信息的描述。因为我们这里并不知道需要recv()的确切长度,所以这里的TCPReceive()也跟recv()一样,有数据就返回。需要验证数据长度的,比如echo服务,我们另外写验证长度的代码。
最后需要说明的是,虽然SYN和FIN都会占用一个字节的数据,但是对于应用层的send()和recv()来说是不可见的。FIN会让recv()返回0,表示连接正常断开。
七。TCP连接的关闭
TCP连接一旦建立,服务器端和客户端就成为了对等关系,任何一方都可以发出关闭握手请求,甚至可以同时发出关闭握手请求。TCP的连接建立需要3次握手,而正常关闭则需要4次握手。
1、主动关闭的一方A调用close(),SendQ不再接收send()写入信息,在SendQ队列的最后,向被动关闭的一方发送TCP的IP数据报作为关闭握手的请求。这个数据报中包含着标志FIN,也包含着此刻的字节序号m。
2、B接收到第一次关闭握手请求后马上返回一个数据报作为回应。因为B接收到了FIN作为关闭连接的一个字节的数据,所以返回的字节序号是m+1。当A接收到B的这个回应,也即是第二次握手以后,表明确认在A到B的方向上不再有数据传送,A即转入所谓半关闭状态,等待B的关闭请求。而B收到FIN会导致recv()返回零,让应用层知道A到B的连接已经断开。
3、B方通知了应用层后也就进入等待关闭的状态。当B开始进入关闭流程,也会由B向A发送一个FIN,同时包含着B到A通讯方向上此刻的字节序号n。
4、A接收到B的这个FIN之后,也会将序号n+1反馈给B,自此,表明B到A的方向上不再有数据传送,TCP连接正式成功关闭。
以上只是对TCP连接关闭的简单描述,事实上,除了使用close()关闭,还可以使用shutdown(),这样在“半关闭”状态下还可以对TCP做其他的利用,具体内容就请大家自己查阅相关资料了。
最后,送上本人对于TCP连接的理解——“双向的单行道”——分别建立连接,也分别断开连接。
Linux下Socket编程之TCP原理_第1张图片

 

你可能感兴趣的:(Linux下Socket编程之TCP原理)