后台开发核心技术(八)Socket通信之TCP

网络通信如何唯一标识一个进程
传输层的”协议+端口“可以唯一标识主机的进程。

关于socket的TCP传输过程的参考博文

Socket

套接字格式:

	{protocol,src_addr,src_port,dest_addr,dest_port}。

这常被称为套接字的五元组。其中protocol指定了是TCP还是UDP连接,其余的分别指定了源地址、源端口、目标地址、目标端口。

网络中的进程是通过socket来通信的。socket是”open->write/read->close"模式的一种实现。socket为这些操作提供了以下一些函数接口

后台开发核心技术(八)Socket通信之TCP_第1张图片

  1. socket() server socket:{protocal}

     int socket(int domain,int type,int protocal);
    

    socket对应于普通文件的打开操作,普通文件的打开返回一个文件描述字,而socket()返回一个socket描述符(整数类型的值),唯一标识一个socket。失败返回INVALID_SOCKET(Linux中返回-1)。

    1 ) domain
    即协议族,决定了socket的地址类型。如AF_INET决定了要用ipv4与端口号的结合。

    2 ) type
    指定socket类型,常用的有:SOCK_STREAM(提供面向稳定的连接数据传输)、SOCK_DGRAM(表示使用不连续、不可靠的数据包连接)。

    3 ) protocal
    指定协议。常用的有:IPPROTP_TCP、IPPROTO_UDP等。对应TCP、UDP。

    (注意type和protocal不一定可以随意组合,比如SOCK_STREAM就不能和IPPROTP_UDP组合)。

    当socket()创建一个socket时,返回的socket描述字存在于协议族(AF_XXX)中,但没有一个具体地址。如果想要赋予地址,必须调用bind(),否则系统就在调用connect()、listen()时自动随机分配一个端口。

  2. bind() server socket:{protocal,src_addr,src_port}

     int bind(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    

    bind()将地址族中的一个特定地址赋给socket(),例如对应AF_INET就是将ipv4地址和端口号组合赋给socket。

    1 ) sockft
    即socket描述字。
    2 ) addr
    一个const struct sockaddr*指针,指向要定给socket的协议地址。这个地址结构根据地址创建socket时的地址协议族不同而不同,如ipv4对应的是如下的代码:

     struct sockaddr_in{
     	sa_familly_t sin_family; //address family:AF_INET
     	in_port_t 	sin_port;	//port in network byte order
     	struct in_addr sin_addr; //internet address
     };
     struct in_addr{
     	uint32_t s_addr;//address in network byte order
     };
    

    3 ) addrlen
    对应的是地址的长度。

  3. listen() 和 connect()
    server socket:{protocal,src_addr,src_port,dest_addr,dest_addr)
    clinet socket:{protocal,src_addr,src_port,dest_addr,dest_addr)

    作为服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket;如果客户端调用connect()发出连接请求,服务器端就会接收到这个请求。

     int listen(int sockfd,int backlog);
     int connect(int sockfd,const struct sockaddr *addr,socklen_t addrlen);
    

    listen()第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()创建的socket默认是一个主动类型的,listen()将socket变为被动类型的,等待客户的连接请求。

    connect函数的第一个参数即为客户端socket描述字,第二个参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。

  4. accept()
    TCP服务器依次调用socket()、bind()、listen()后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connnet()之后就会向TCP服务器发送一个连接请求。TCP服务器监听到请求之后,就会调用accept()接受,这样连接就建立好了。

     int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);
    

    accept()第一个参数为服务器的socket描述字;第二个参数用于返回客户端的协议地址;第三个参数为协议的长度。如果accept成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

    注意:accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()生成的,为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常只创建一个监听socket描述字,在服务器生命周期内一直存在。内核为每个由服务器进程接受的客户创建了一个已连接socket描述字。
    后台开发核心技术(八)Socket通信之TCP_第2张图片 连接还是那个连接,只不过服务器偷偷地换掉了这个tcp连接所关联的套接字和文件描述符,而客户端并不知道这一切。

  5. read()和write()
    至此服务器与客户端已经建立好连接了,可以调用网络I/O进行读写操作。

     ssize_t read(int fd,void *buf,size_t count);
     ssize)t write(int fd,const void *buf,size_t count);
    

    read()负责从fd中读取内容。当读取成功时,read()返回实际所读字节数,如果为0表示已经读到文件的结束了,小于0出错。第一个参数为socket描述字fd;第二个参数为缓冲区buf;第三个参数为缓冲区长度count。

    write()负责将buf中的nbytes字节内容写入文件描述符fd成功时返回写的字节数。返回值大于0表示写了部分或者全部数据,小于0发生错误。三个参数分别是:fd表示socket描述字;buf表示缓冲区;count表示缓冲区长度。

  6. close()
    完成了读写操作后,关闭相应的socket描述字。需要包含
    #include

     int close(int fd);
    

    close一个TCP socket的默认行为时,会把该socket标记为关闭,该描述字不能再由调用进程使用,也就是不能在作为read或write的第一个参数。

    注意close()只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,会出发TCP客户端向服务器发送终止连接请求。

关于Socket的TCP通信的一些疑问

  1. close()使对应socket描述字引用计数-1——为什么socket描述字会有大于1的引用计数?

    思考:socket的地址唯一标志了某个主机的某个进程,但进程本身可能有多个子进程,这几个子进程都通过相同的socket描述字进行tcp连接。这使得对于同一个socket描述字有大于1的引用计数。

  2. 在TCP4次挥手过程中,是如何发起主动close请求的?
    TCP的3次握手与4次挥手参考博文

    思考:
    客户端主动发起close(),服务器隐式调用close(): 后台开发核心技术(八)Socket通信之TCP_第3张图片
    服务器主动发起close(),客户端隐式调用close():
    后台开发核心技术(八)Socket通信之TCP_第4张图片
    也就是说,当某一端的套接字引用次数为0时,会发出终止连接请求。这是因为所有子进程都close()了这个套接字。对于客户端来说,好比关闭了一个网页;对于服务器来说,好比404。而另一端在接收到主动close()请求后,在ack的同时,自己也会隐式调用close()。

  3. socket用来唯一标识某主机的某个端口,但是同一主机+端口可以有多个socket吗?

    思考:
    实际上服务器在accpet()以后,监听的socket和已连接的socket是两个不同的socket。但是它们都标志了相同的地址。在建立连接之后,read()和write()可以由两端调用,当进程没有被close()时,其第一个参数就是socket的描述符。(注意此时的socket是两边的连接socket)

  4. 服务器关闭socket指的是哪个socket?

    思考:在accept()之后,用于tcp通信的socket已经由监听socket,改成了已连接socket,在read()、write()的过程中也是如此,那么close()自然而然也是已连接socket。监听socket直到整个服务器生命周期结束才会关闭。

你可能感兴趣的:(后台开发核心技术(八)Socket通信之TCP)