Linux网络编程-3-socket编程api

基于socket的网络应用概述

以一个C/S结构的网络应用为例,客户端和服务器端使用socket通信的流程图如下:

从Linux内核的角度来看,一个套接字就是通信的一个端点。从Linux程序的角度来看,套接字就是一个有相应描述符的文件。(注:Linux中有普通文件、目录、套接字三种,Linux中一切皆是文件)

这里的read和write就很好的表现了Linux一切皆为文件的思想,进行网络通信,其实也就是对socket这个文件进行通用的Unix I/O操作。

Linux网络编程-3-socket编程api

socket通信的过程可以理解为打电话:socket为电话机。

  1. 建立连接(三次握手)

connect是拨号;服务器端的 bind 就好比是去电信公司开户,将电话号码和我们家里的电话机绑定,这样别人就可以用这个号码找到你; listen 是人们在家里听到了响铃;accept 是被叫的一方拿起电话开始应答。(自此,三次握手就完成了,连接建立完毕。)

  1. 交流(数据传输过程)

write就是给对方说话,read就是听对方说了什么。

  1. 关闭连接(四次挥手)

最后,拨打电话的人完成了此次交流,挂上电话,对应的操作可以理解为 close,接听电话 的人知道对方已挂机,也挂上电话,也是一次 close。

Linux网络编程-3-socket编程api

下面是偏教科书式的理解:

  1. 三次握手

服务器端通过 socket,bind 和 listen 完成了被动套接字的准备 工作,被动的意思就是等着别人来连接,然后调用 accept,就会阻塞在这里,等待客户端的连接来临;客户端通过调用 socket 和 connect 函数之后,也会阻塞。接下来的事情是 由操作系统内核完成的,更具体一点的说,是操作系统内核网络协议栈在工作。

下面是具体的过程:

(a) 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 j,客户端 进入 SYNC_SENT 状态;

(b)服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 j+1,表示 对 SYN 包 j 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为 k,服务器端进入 SYNC_RCVD 状态;

(c)客户端协议栈收到 ACK 之后,使得应用程序从 connect 调用返回,表示客户端到服务

器端的单向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对

服务器端的 SYN 包进行应答,应答数据为 k+1;

(d)应答包到达服务器端后,服务器端协议栈使得 accept 阻塞调用返回,这个时候服务器

端到客户端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。

  1. 数据传输过程

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

  1. 四次挥手

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

Linux中与socket相关api

socket()-创建socket

要创建一个可用的套接字,需要使用下面的函数:

#include <sys/types.h>
#include <sys/socket.h>
		
//成功返回文件描述符,失败返回-1
int socket(int domain, int type,int protocol);

domain 就是指 PF_INET、PF_INET6 以及 PF_LOCAL 等,表示什么样的套接字。

type 可用的值是:

  • SOCK_STREAM: 表示的是字节流,对应 TCP;
  • SOCK_DGRAM: 表示的是数据报,对应 UDP;
  • SOCK_RAW: 表示的是原始套接字。
  • 自Linux2.6.17版本之后,可以为SOCK_NONBLOCK(非阻塞)和SOCK_CLOEXEC(用fork调用创建子进程时在子进程中关闭该socket)。

参数 protocol 原本是用来指定通信协议的,但现在基本废弃。因为协议已经通过前面两个参数指定完成。protocol 目前一般写成 0 即可。

connect()-发起连接

客户端和服务器端的连接建立,是通过 connect 函数完成的。

#include <sys/types.h>
#include <sys/socket.h>

//成功返回0,失败返回-1;
int connect(int sockfd, const struct sockaddr* serv_addr,socklen_t addrlen);	

函数的第一个参数 sockfd 是连接套接字。

第二个、第三个参数 servaddr 和 addrlen 分别代表指向套接字地址结构的指针和该结构的大小。套接字地址结构必须含有服务器的 IP 地址和端口号。

客户在调用函数 connect 前不必非得调用 bind 函数,因为如果需要的话,内核会确定源 IP 地址,并按照一定的算法选择一个临时端口作为源端口。

connect调用成功返回0。一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信。connect失败则返回-1并设置errno,常见的两种是ECONNREFUSED和ETIMEDOUT:

  • ECONNREFUSED:目标端口不存在,连接被拒绝。
  • ETIMEDOUT:连接超时。

bind()-命名socket

创建socket时,指定了地址族,但是并未指定使用该地址族中的那个具体socket地址。将一个socket与socket地址绑定称为给socket命名。在服务器程序中,通常要命名socket。

#include <sys/types.h>
#include <sys/socket.h>

//成功返回0,失败返回-1
int bind(int sockfd, const struct sockaddr* myaddr,socklen_t addrlen);		

bind 函数后面的第二个参数是通用地址格式sockaddr* addr。这里虽然接收的是通用地址格式,实际上传入的参数可能是 IPv4、IPv6 或者本地套接字格式。bind 函数会根据 len 字段判断传入的参数 addr 该怎么解析,len 字段表示的就是传入的地址长度,它是一个可变值。

对于使用者来说,每次需要将 IPv4、IPv6 或者本地套接字格式转化为通用套接字格式,就像下面的 IPv4 套接字地址格式的例子一样:

struct sockaddr_in name;
bind (sock, (struct sockaddr *) &name, sizeof (name)

将地址设置成本机的 IP 地址,相当告诉操作系统内核,仅仅对目标 IP 是本机 IP 地址的 IP 包进行处理。

但是这样写的程序在部署时有一个问题,如果不清楚自己的应用程序将会被部署到哪台机器上,可以利用通配地址的能力帮助我们解决这个问题。

对于 IPv4 的地址来说,使用 INADDR_ANY 来完成通配地址的设置;对于 IPv6 的地址来说,使用 IN6ADDR_ANY 来完成通配地址的设置。

struct sockaddr_in name;
name.sin_addr.s_addr = htonl (INADDR_ANY); /* IPV4 通配地址 */
listen()-监听socket

初始化创建的套接字,可以认为是一个"主动"套接字,其目的是之后主动发起请求。通过 listen 函数,可以将原来的"主动"套接字转换为"被动"套接字,告诉操作系统内核,这个套接字用于“被”请求。

#include <sys/socket.h>

//成功返回0,失败返回-1并设置errno
int listen(int sockfd, int backlog);

第一个参数 socketfd 为套接字描述符。

第二个参数 backlog提示内核监听队列的最大长度。监听队列的长度如果超过backlog,服务器将不受理新的客户连接,客户端也将受到ECONNREFUSED错误信息。(在内核版本2.2之前的Linux中,backlog参数是指所有处于半连接状态和完全连接状态的socket的上限。在此之后,他只表示处于完全连接状态的socket上限,处于半连接状态的socket的上限则由/proc/sys/net/ipv4/tcp_max_syn_backlog内核参数定义)。典型值为5

accept()-接受连接

accept 这个函数的作用就是连接建立之后,操作系统内核和应用程序之间的桥梁。

#include <sys/types.h>
#include <sys/socket.h>

//成功返回一个新的连接socket,失败返回-1;
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);

sockfd参数是执行过listen系统调用的监听socket。

addr参数用来获取被接受端socket地址,该socket地址的长度由addrlen参数指出。

accept成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信。accept失败返回-1并设置errno。
accetp只是从监听队列中取出连接,而不论连接处于何种状态,更不关心任何网络状况的变化。

reference

[1] 深入理解计算机系统

[2] 极客时间 · 网络编程实战

[3] Linux高性能服务器编程

Linux网络编程-3-socket编程api

上一篇:AcWing 第15场周赛


下一篇:C# 向类中添加回调和事件