Socket通信原理

  网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个Socket。Socket本质是编程接口(API),通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同计算机之间的通信。

  应用程序通常通过"套接字"向网络发出请求或者应答网络请求,Socket在建立网络连接时使用。连接成功时,应用程序两端都会产生一个Socket实例,操作这个实例,完成所需的会话。在Internet上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。

  Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面。

Socket通信原理

1、网络进程通信

  如何标识网络中的一个进程呢?在本地可以通过进程PID来标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的IP地址可以唯一标识网络中的主机,而传输层的协议和端口可以唯一标识主机中的应用程序(进程)。这样利用三元组(IP地址,协议,端口)就可以标识网络中的进程了。网络中进程之间的通信就利用socket来完成。

2、通信过程

Socket通信原理

(1)服务器端初始化socket()

(2)绑定端口bind()

(3)监听端口listen()

(4)调用accept()阻塞,等待客户端连接

(5)客户端初始化一个socket()

(6)连接服务器connect()

(7)客户端/服务器端I/O读写

(8)关闭连接close()

3、socket接口函数

3.1  socket()函数。用于创建一个socket套接字描述符,唯一标识一个socket。

int socket(int domain, int type, int protocol)

  • domain:即协议域,又称为协议族。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址。如AF_INET决定了要用IPV4地址(32位)与端口号(16位)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
  • type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。
  • protocol:指定传输协议。常用的协议有IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。
  如果调用成功就返回新创建的套接字的描述符,如果失败就返回INVALID_SOCKET(Linux下失败返回-1)。套接字描述符是一个整数类型的值。每个进程的进程空间里都有一个套接字描述符表,该表中存放着套接字描述符和套接字数据结构的对应关系。该表中有一个字段存放新创建的套接字的描述符,另一个字段存放套接字数据结构的地址,因此根据套接字描述符就可以找到其对应的套接字数据结构。每个进程在自己的进程空间里都有一个套接字描述符表,但是套接字数据结构都是在操作系统的内核缓冲里。

3.2  bind()函数。把一个地址族中的特定地址赋给socket。例如对应AF_INET就是把一个IPV4地址和端口号组合赋给socket。

int bind(SOCKET socket, const struct sockaddr* address, socklen_t address_len)

函数的三个参数分别为:

  • socket:套接字描述符,它是通过socket()函数创建,唯一标识一个socket。bind()函数就是给这个套接字描述符绑定一个名字。
  • address:是一个sockaddr结构指针,包含了要结合的IP地址和端口号。
  • address_len:address缓冲区的长度。

如果函数执行成功,返回值为0,否则为SOCKET_ERROR。

3.3  listen()、connect()函数

如果是服务器,在调用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地址的长度。

3.4  accept()函数

TCP客户端调用connect()函数,就会向TCP服务器端发送一个连接请求。TCP服务器端监听到这个请求后,就会调用accept()函数接收请求。

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

accept()函数的第一个参数是服务器端的socket套接字描述符,第二个参数是指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数是协议地址的长度。

如果accpet成功,那么返回值是由内核自动生成的一个新的socket套接字描述符,服务器端把这个新的套接字描述符发给客户端,一旦客户端确认了此描述,连接就建立好了。而服务器端套接字继续处于监听状态,继续接收其他客户端套接字的连接请求。

注意:accept()函数的第一个参数是服务器开始调用socket()函数生成的套接字描述符,称为监听socket描述符;而accept()函数返回的是已连接的socket描述符。一个服务器通常仅仅创建一个监听socket描述符,它在该服务器的生命周期内一直存在。内核为服务器进程接收的客户端连接分别创建一个已连接socket描述符,当服务器完成了对某个客户端的服务,相应的已连接socket描述符就被关闭。

3.5  read()、write()等函数

调用网络I/O进行读写操作,有下面几组:

  • read()/write()
  • recv()/send()
  • readv()/writev()
  • recvmsg()/sendmsg()
  • recvfrom()/sendto()

3.6  close()函数

在服务器端与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述符。