# socket 简介

​ 套接字 (Socket),就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。从所处的地位来讲,套接字上联应用进程,下联网络协议栈,是应用程序通过网络协议进行通信的接口,是应用程序与网络协议栈进行交互的接口。

​ 套接字是通信的基石,是支持 TCP/IP 协议的路通信的基本操作单元。可以将套接字看作不同主机间的进程进行双间通信的端点,它构成了单个主机内及整个网络间的编程界面。套接字存在于通信域中,通信域是为了处理一般的线程通过套接字通信而引进的一种抽象概念。套接字通常和同一个域中的套接字交换数据 (数据交换也可能穿越域的界限,但这时一定要执行某种解释程序),各种进程使用这个相同的域互相之间用 Internet 协议簇来进行通信。

​ 套接字 (Socket) 可以看成是两个网络应用程序进行通信时,各自通信连接中的端点,这是一个逻辑上的概念。它是网络环境中进程间通信API (应用程序编程接口),也是可以被命名和寻址的通信端点,使用中的每一个套接字都有其类型和一个与之相连进程。通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 Socket 中,该 Socket 通过与网络接口卡 ( NIC ) 相连的传输介质将这段信息送到另外一台主机的 Socket 中,使对方能够接收到这段信息。 Socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制 。

# 表示方法

​ 套接字 Socket=( IP 地址:端口号),套接字的表示方法是点分十进制的 lP 地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。例如:如果 IP 地址是 210.37.145.1,而端口号是 23,那么得到套接字就是 ( 210.37.145.1:23 )

# 主要类型

# 流套接字(SOCK_STREAM

​ 流套接字用于提供面向连接、可靠的数据传输服务。该服务将保证数据能够实现无差错、无重复送,并按顺序接收。流套接字之所以能够实现可靠的数据服务,原因在于其使用了传输控制协议,即 TCP (The Transmission Control Protocol) 协议

# 数据报套接字( SOCK_DGRAM

​ 数据报套接字提供一种无连接的服务。该服务并不能保证数据传输的可靠性,数据有可能在传输过程中丢失或出现数据重复,且无法保证顺序地接收到数据。数据报套接字使用 UDP( User DatagramProtocol) 协议进行数据的传输。由于数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失情况,需要在程序中做相应的处理

# 原始套接字(SOCK_RAW

​ 原始套接字与标准套接字 (标准套接字指的是前面介绍的流套接字和数据报套接字) 的区别在于:原始套接字可以读写内核没有处理的 IP 数据包,而流套接字只能读取 TCP 协议的数据,数据报套接字只能读取 UDP 协议的数据。因此,如果要访问其他协议发送的数据必须使用原始套接

# 工作流程

​ 要通过互联网进行通信,至少需要一对套接字,其中一个运行于客户端,我们称之为 Client Socket,另一个运行于服务器端,我们称之为 Server Socket 。

根据连接启动的方式以及本地套接字要连接的目标,套接字之间的连接过程可以分为三个步骤 :

(1) 服务器监听。

(2) 客户端请求。

(3) 连接确认。

# 1. 服务器监听

​ 所谓服务器监听,是指服务器端套接字并不定位具体的客户端套接字,而是处于等待连接的状态,实时监控网络状态 。

# 2. 客户端请求

​ 所谓客户端请求,是指由客户端的套接字提出连接请求,要连接的目标是服务器端的套接字。为此,客户端的套接字必须首先描述它要连接的服务器的套接字,指出服务器端套接字的地址和端口号,然后就向服务器端接字提出连接请求 。

# 3. 连接确认

​ 所谓连接确认,是指当服务器端套接字监听到或者说接收到客户端套接字的连接请求,就会响应客户端套接字的请求,建立一个新的线程,并把服务器端套接字的描述发送给客户端。一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,接收其他客户端套接字的连接请求

# 使用方法

# 1. 创建一个 socket

创建函数int socket(int family, int type, int protocol) ;
​ socket () 打开一个网络通讯端口,如果成功的话,就像 open () 一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据,如果 socket () 调用出错则返回 - 1。对于 IPv4 ,family 参数指定为 AF_INET 。对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM ,表示面向数据报的传输协议。protocol 参数的介绍从略,指定为 0 即可。

# 2. 绑定本机 IP 端口
# 3. bind() 绑定

bind () 函数int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen) ;
​ 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用 bind 绑定一个固定的网络地址和端口号。bind () 成功返回 0,失败返回 - 1

​ bind () 的作用是将参数 sockfdmyaddr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符 (创建 socket 时的返回值) 监听 myaddr 所描述的地址和端口号 (对结构体初始化时所赋值)。 struct sockaddr * 是一个通用指针类型, myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度

​ 程序中对 myaddr 参数是这样初始化的:

memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(5188);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); 

​ 首先将整个结构体清零(也可以用 bzero 函数),然后设置地址类型为 AF_INET ,网络地址为 INADDR_ANY ,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址,端口号为 5188,可以自行定义,范围是从 1024 到 65535。

# 4. 等待客户端的连接,接收客户端数据

监听函数int listen(int sockfd, int backlog) ;
​ 典型的服务器程序可以同时服务于多个客户端,当有客户端发起连接时,服务器调用的 accept() 返回并接受这个连接,如果有大量的客户端发起连接而服务器来不及处理,尚未 accept 的客户端就处于连接等待状态,listen () 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。 listen() 成功返回 0,失败返回 - 1。

接受连接int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen) ;
​ 三方握手完成后,服务器调用 accept () 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。 cliaddr 是一个传出参数, accept() 返回时传出客户端的地址和端口号。 addrlen 参数是一个传入传出参数( value-result argument ),传入的是调用者提供的缓冲区 cliaddr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 cliaddraddrlen 参数传 NULL,表示不关心客户端的地址。

# 5. 数据接受与发送

调用网络 I/O 进行读写操作:

  • read()/write()

  • recv()/send()

  • readv()/writev()

  • recvmsg()/sendmsg()

  • recvfrom()/sendto()

    recv()/send() 详解

    函数int recv( SOCKET s, char FAR *buf, int len, int flags ) ;

    不论是客户还是服务器应用程序都用 recv 函数从 TCP 连接的另一端接收数据。

    (1)第一个参数指定接收端套接字描述符;

    (2)第二个参数指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据;

    (3)第三个参数指明 buf 的长度;

    (4)第四个参数一般置 0。

    ​ 这里只描述同步 Socket 的 recv 函数的执行流程。当应用程序调用 recv 函数时, recv 先等待 s 的发送缓冲中的数据被协议传送完毕,如果协议在传送 s 的发送缓冲中的数据时出现网络错误,那么 recv 函数返回 SOCKET_ERROR,如果 s 的发送缓冲中没有数据或者数据被协议成功发送完毕后, recv 先检查套接字 s 的接收缓冲区,如果 s 接收缓冲区中没有数据或者协议正在接收数据,那么 recv 就一直等待,只到协议把数据接收完毕。当协议把数据接收完毕, recv 函数就把 s 的接收缓冲中的数据 copy 到 buf 中(注意协议接收到的数据可能大于 buf 的长度,所以在这种情况下要调用几次 recv 函数才能把 s 的接收缓冲中的数据 copy 完。 recv 函数仅仅是 copy 数据,真正的接收数据是协议来完成的), recv 函数返回其实际 copy 的字节数。如果 recv 在 copy 时出错,那么它返回 SOCKET_ERROR;如果 recv 函数在等待协议接收数据时网络中断了,那么它返回 0。

    函数int send( SOCKET s, const char FAR *buf, int len, int flags ) ;

    ​ 不论是客户还是服务器应用程序都用 send 函数来向 TCP 连接的另一端发送数据。

    ​ 客户程序一般用 send 函数向服务器发送请求,而服务器则通常用 send 函数来向客户程序发送应答。

    (1)第一个参数指定发送端套接字描述符;

    (2)第二个参数指明一个存放应用程序要发送数据的缓冲区;

    (3)第三个参数指明实际要发送的数据的字节数;

    (4)第四个参数一般置 0。

    这里只描述同步 Socket 的 send 函数的执行流程。当调用该函数时,send 先比较待发送数据的长度 len 和套接字 s 的发送缓冲的长度, 如果 len 大于 s 的发送缓冲区的长度,该函数返回 SOCKET_ERROR;如果 len 小于或者等于 s 的发送缓冲区的长度,那么 send 先检查协议是否正在发送 s 的发送缓冲中的数据,如果是就等待协议把数据发送完,如果协议还没有开始发送 s 的发送缓冲中的数据或者 s 的发送缓冲中没有数据,那么 send 就比较 s 的发送缓冲区的剩余空间和 len ,如果 len 大于剩余空间大小 send 就一直等待协议把 s 的发送缓冲中的数据发送完,如果 len 小于剩余空间大小 send 就仅仅把 buf 中的数据 copy 到剩余空间里(注意并不是 send 把 s 的发送缓冲中的数据传到连接的另一端的,而是协议的,send 仅仅是把 buf 中的数据 copy 到 s 的发送缓冲区的剩余空间里)。

    如果 send 函数 copy 数据成功,就返回实际 copy 的字节数,如果 send 在 copy 数据时出现错误,那么 send 就返回 SOCKET_ERROR;如果 send 在等待协议传送数据时网络断开的话,那么 send 函数也返回 SOCKET_ERROR。

    要注意 send 函数把 buf 中的数据成功 copy 到 s 的发送缓冲的剩余空间里后它就返回了,但是此时这些数据并不一定马上被传到连接的另一端。如果协议在后续的传送过程中出现网络错误的话,那么下一个 Socket 函数就会返回 SOCKET_ERROR。(每一个除 send 外的 Socket 函数在执行的最开始总要先等待套接字的发送缓冲中的数据被协议传送完毕才能继续,如果在等待时出现网络错误,那么该 Socket 函数就返回 SOCKET_ERROR)。

    使用声明:

    #include 
    
           ssize_t read(int fd, void *buf, size_t count);
           ssize_t write(int fd, const void *buf, size_t count);
    
           #include 
           #include 
    
           ssize_t send(int sockfd, const void *buf, size_t len, int flags);
           ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
           ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                          const struct sockaddr *dest_addr, socklen_t addrlen);
           ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                            struct sockaddr *src_addr, socklen_t *addrlen);
    
           ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
           ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
    

    ​ read 函数是负责从 fd 中读取内容。当读成功时,read 返回实际所读的字节数,如果返回的值是 0 表示已经读到文件的结束了,小于 0 表示出现了错误。如果错误为 EINTR 说明读是由中断引起的,如果是 ECONNREST 表示网络连接出了问题。

    ​ write 函数将 buf 中的 nbytes 字节内容写入文件描述符 fd. 成功时返回写的字节 数。失败时返回 - 1,并设置 errno 变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1) write 的返回值大于 0,表示写了部分或者是 全部的数据。2) 返回的值小于 0,此时出现了错误。我们要根据错误类型来处理。如果错误为 EINTR 表示在写的时候出现了中断错误。如果为 EPIPE 表示 网络连接出现了问题 (对方已经关闭了连接)。

# socket 中 TCP 的三次握手建立连接详解

tcp 建立连接要进行 “三次握手”,即交换三个分组。大致流程如下:

  • 客户端向服务器发送一个 SYN J
  • 服务器向客户端响应一个 SYN K ,并对 SYN J 进行确认 ACK J+1
  • 客户端再想服务器发一个确认 ACK K+1

只有就完了三次握手,但是这个三次握手发生在 socket 的那几个函数中呢?请看下图:

image