网络编程
引子
TCP / IP 分层模型:
| 层 | 协议 |
|---|---|
| 应用层 | HTTP FTP ... |
| 传输层 | TCP UDP ... |
| 网络层 | IPv4 IPv6 ... |
| 物理层 | LAN WLAN ... |
计算机组成原理:
| 设备 | 功能 |
|---|---|
| 数据总线 | 传输数据 |
| IO设备 | 输入输出 |
| 主存储器 | 存储数据 |
| CPU | 计算数据 |
操作系统:
-
(内核空间)管理计算机硬件资源进行管理和控制
-
(用户空间)提供系统调用接口,供用户程序调用
-
进程:为了让计算机能够同时运行多个程序,操作系统引入了进程的概念
-
线程:线程是 CPU 调度的基本单位,进程是资源分配的基本单位
参考
-
书籍
- 《Linux 多线程服务端编程:使用 muduo C++ 网络库》
- 《Linux 高性能服务器编程》 游双
-
博客
1 阻塞 IO
当 进程 发起 IO请求 后,如果 内核 没有准备好数据,那么 进程 将一直等待,直到 内核 准备好数据为止
当程序发起发送数据的请求时,如果发送缓冲区已满,那么程序将一直等待,直到发送缓冲区有空间为止
当程序发起接收数据的请求时,如果接收缓冲区为空,那么程序将一直等待,直到接收缓冲区有数据为止
TCP
TCP 通信模型
socket 是一个接口,而不是一种协议,其抽象在应用层与传输层之间
| 函数 | 服务端 | |
|---|---|---|
| 1 | socket() |
创建 socket |
| 2 | bind() |
绑定 ip + port 至该 socket 上 |
| 3 | listen() |
监听该 端口 |
| 4 | accept() |
接受来自客户端的连接请求 |
| 5 | recv() |
从 socket 中读取字符 |
| 6 | close() |
关闭 socket |
| 函数 | 客户端 | |
|---|---|---|
| 1 | socket() |
创建 socket |
| 2 | connect() |
连接指定 ip + port |
| 3 | send() |
发送消息 |
| 4 | close() |
关闭 socket |
server
-
创建套接字
socket()C++ 1#include <sys/socket.h> // socket()C++ 1int server = socket(AF_INET, SOCK_STREAM, 0); // 创建套接字当
socket()函数调用成功时,返回一个文件描述符,即socket,当调用失败时,返回-1AF_INET:IPv4SOCK_STREAM:TCP0:自动选择协议,这里是TCP协议
-
绑定 ip + port
bind()C++ 1 2
#include <cstring> // memset() #include <arpa/inet.h> // sockaddrC++ 1 2 3 4 5 6 7 8 9
// 服务端地址 sockaddr_in server_addr; memset(&server_addr, '\0', sizeof server_addr); // 给地址赋值 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); server_addr.sin_port = htons(10086); // 绑定 bind(server, (sockaddr*)&server_addr, sizeof server_addr);当
bind()函数调用成功时,返回0,当调用失败时,返回-1sockaddr_in:IPv4 套接字地址结构体sin_family:地址族,这里是 IPv4sin_addr.s_addr:IPv4 地址sin_port:端口号
为什么要强制转换为
sockaddr? 为什么要使用sockaddr_in而不是sockaddr? *sockaddr是一个通用的套接字地址结构体,可以用于任何类型的套接字 *sockaddr_in是一个 IPv4 套接字地址结构体,只能用于 IPv4 套接字 *sockaddr_in是sockaddr的子类,可以强制转换为sockaddr* 除了sockaddr_in,还有sockaddr_in6、sockaddr_un等
-
监听
listen()
> 成功:返回0> 失败:返回-1C++ 1 2
// 监听 listen(server, 0);0:等待队列的最大长度,目前无需关注
-
接受连接请求
accept()
> 成功:返回一个文件描述符,fd > 0> 失败:返回-1C++ 1 2 3
sockaddr_in client_addr; socklen_t client_addr_len = sizeof client_addr; int client = accept(server, (sockaddr*)&client_addr, &client_addr_len);socklen_t:sockaddr的长度类型
为什么这里需要传入
&client_addr_len而不是client_addr_len? *accept()函数会修改client_addr_len的值,所以需要传入指针
-
接收消息
recv()
> 成功:返回接收的字节数 > 错误:返回-1> 失败:返回0,对端关闭连接C++ 1 2 3
char buf[1024]; memset(buf, '\0', sizeof buf); recv(server, buf, sizeof buf, 0);通常,
recv()函数返回接收到的字节数,对端关闭返回0,其余返回-1
-
关闭套接字
close()C++ 1close(server);
client
-
创建套接字
socket() -
连接指定 ip + port
connect()
> 成功:返回0> 失败:返回-1C++ 1 2 3 4 5 6 7 8
sockaddr_in server_addr; memset(&server_addr, '\0', sizeof server_addr); // 给地址赋值 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_port = htons(10086); // 连接 connect(client, (sockaddr*)&server_addr, sizeof server_addr); -
发送消息
send()
> 成功:返回发送的字节数 > 错误:返回-1> 失败:返回0,对端关闭连接C++ 1 2 3 4
char buf[1024]; memset(buf, '\0', sizeof buf); scanf("%s", buf); send(client, buf, strlen(buf), 0); -
关闭套接字
close()C++ 1close(client);
send 和 recv
> 0发送或接收的字节数= 0对端已经关闭连接< 0出现了错误
TCP 三握四挥
-
连接
客户端请求连接,connect阻塞服务端收到请求,accept阻塞,同时向客户端发送确认信息客户端确认;connect返回;同时发送信息;accept返回
-
断开
客户端请求断开,close阻塞服务端发送确认信息(我已知晓)服务端发送请求信息(请求断开),close阻塞客户端收到确认信息,发送确认信息,俩close先后返回
-
连接之后
客户端发送信息,send阻塞服务端接收信息,recv阻塞服务端发送信息,send阻塞客户端接收信息,recv阻塞- 重复 1-4,直到
客户端或服务端断开连接
UDP
UDP 通信模型
| 函数 | 服务端 | |
|---|---|---|
| 1 | socket() |
创建 socket |
| 2 | bind() |
绑定 ip + port 至该 socket 上 |
| 3 | recvfrom() |
接收来自客户端的消息 |
| 4 | close() |
关闭 socket |
| 函数 | 客户端 | |
|---|---|---|
| 1 | socket() |
创建 socket |
| 2 | sendto() |
发送消息 |
| 3 | close() |
关闭 socket |
server
-
创建套接字
socket()*C++ 1int server = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字SOCK_DGRAM:UDP *0:自动选择协议,这里是UDP协议 -
绑定 ip + port
bind()C++ 1 2 3 4 5 6 7 8 9
// 服务端地址 sockaddr_in server_addr; memset(&server_addr, '\0', sizeof server_addr); // 给地址赋值 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("0.0.0.0"); server_addr.sin_port = htons(10086); // 绑定 bind(server, (sockaddr*)&server_addr, sizeof server_addr); -
接收消息
recvfrom()C++ 1 2 3 4 5 6 7 8 9
// 缓冲区 char buf[1024]; memset(buf, '\0', sizeof buf); // 客户端地址 sockaddr_in client_addr; memset(&client_addr, '\0', sizeof client_addr); socklen_t client_addr_len = sizeof client_addr; // 接收消息 recvfrom(server, buf, sizeof buf, 0, (sockaddr*)&client_addr, &client_addr_len); -
关闭套接字
close()C++ 1close(server);
client
-
创建套接字
socket()C++ 1int server = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字 -
发送消息
sendto()C++ 1 2 3 4 5 6 7 8 9 10 11 12
// 服务端地址 sockaddr_in server_addr; memset(&server_addr, '\0', sizeof server_addr); // 给地址赋值 server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); server_addr.sin_port = htons(10086); // 发送消息 char buf[1024]; memset(buf, '\0', sizeof buf); scanf("%s", buf); sendto(client, buf, strlen(buf), 0, (sockaddr*)&server_addr, sizeof server_addr); -
关闭套接字
close()C++ 1close(client);
Boost.Asio
直接使用其提供的 boost::asio::ip::tcp
server
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | |
client
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
2 非阻塞 IO
当 进程 发起 IO请求 后,即使 内核 没有准备好数据,进程 也将立即返回,不会等待,同时 内核 会返回一个错误码,告诉 进程 为什么没有准备好数据
非阻塞输入
| C++ | |
|---|---|
1 2 3 | |
c2s
此时,我们可以构建一个简单的 c2s 通信模型: 多个 客户端可以与 一个 服务端 收发任意条 消息
server:
- 创建套接字
socket() - 绑定 ip + port
bind() - 监听
listen() -
设置套接字为非阻塞
fcntl()C++ 1#include <fcntl.h> // fcntl()C++ 1fcntl(server, F_SETFL, O_NONBLOCK);fcntl():控制文件描述符属性int fcntl(int fd, int cmd, ... /* arg */ );fd:文件描述符cmd:操作命令,对fd进行操作F_SETFL:设置文件描述符状态标志F_GETFL:获取文件描述符状态标志
arg:操作命令的参数,根据cmd的不同而不同O_NONBLOCK:非阻塞O_ASYNC:异步O_SYNC:同步
-
循环接受连接请求
accept() - 循环接收
recv()和发送send()消息 - 关闭套接字
close()
client:
- 创建套接字
socket() - 连接指定 ip + port
connect() - 设置套接字为非阻塞
fcntl() - 循环发送
send()和接收recv()消息 - 关闭套接字
close()
echo server
echo server,即客户端发送什么,服务端就回复什么 在 c2s 的基础上,初步尝试使用面向对象的思想实现:
SockAddr:套接字地址类Event:事件类,包含事件处理函数Acceptor:接收器类
http server
在 echo server 中,使用 deal 函数处理接收到的消息,如果想处理 http 请求,只需要重写 deal 函数即可: * 获取 http 请求的请求 * 解析 http 请求的请求 * 构造 http 响应的响应 * 发送 http 响应的响应
c2c
之前的 c2s 通信模型,是多个客户端与一个服务端收发任意条消息,现在我们尝试构建一个 c2c 通信模型:
- 每个客户端可以与任意个客户端建立连接
- 每个客户端可以与任意个客户端收发任意条消息
3 复用 IO
在阻塞 IO 中,如何没有连接请求,accept() 函数将一直阻塞,直到有连接请求为止,recv() 和 send() 函数也是如此,如果没有数据,将一直阻塞,直到有数据为止。
在非阻塞 IO 中,我们采取循环的方式,不断的调用 accept()、recv()、send() 函数,如果没有接收到连接请求或数据,那么函数将立即返回,不会等待,同时内核会返回一个错误码,告诉进程为什么没有准备好数据。
在 IO 复用 中,我们可以使用 select()、poll()、epoll() 函数,将多个文件描述符注册到内核中,当有文件描述符准备好数据时,内核将通知进程,进程再调用 accept()、recv()、send() 函数,这样就不需要循环调用这些函数了。
c2s_epoll
在 c2s 的基础上,使用 epoll() 函数实现 IO 复用,客户端是非阻塞的普通客户端。
eventpoll,事件轮询,Linux 内核实现IO多路复用(IO multiplexing)的一个实现
直观来说,I/O 复用的作用就是:让程序能够在单进程、单线程的模式下,同时处理 多个文件描述符 的 I/O 请求
- 底层创建一个 红黑树 和 就绪链表(双向链表)
-
红黑树 存储所监控的文件描述符的节点数据,就绪链表 存储就绪的文件描述符的节点数据
-
epoll_create1创建一个 epoll 文件描述符,事件轮询的实例,返回一个文件描述符,即事件树C++ 1#include <sys/epoll.h> // epoll_create1()C++ 1int epollfd = epoll_create1(0);0:等待队列的最大长度
-
int epoll_ctl(事件树, 操作, 文件描述符, 事件)- 操作:
EPOLL_CTL_ADD:注册新的事件到事件树EPOLL_CTL_MOD:修改已经注册的事件EPOLL_CTL_DEL:删除已经注册的事件
- 事件:
EPOLLIN:对应的文件描述符可以读recv(包括对端SOCKET正常关闭)EPOLLPRI:对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)EPOLLOUT:对应的文件描述符可以写send(包括对端SOCKET正常关闭)EPOLLERR:对应的文件描述符发生错误EPOLLHUP:对应的文件描述符被挂断EPOLLET:将 EPOLL 设为边缘触发(Edge Triggered)模式EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,删除
- 操作:
-
int epoll_wait(事件树, 事件数组, 事件数组大小, 超时时间)- 事件数组:
epoll_event结构体数组 - 超时时间:
-1阻塞,0立即返回,>0等待指定时间
- 事件数组:
一些数据结构
epoll_event事件结构体,用于注册事件C++ 1#include <sys/epoll.h> // epoll_eventC++ 1 2 3 4 5
struct epoll_event { uint32_t events; // 事件类型 epoll_data_t data; // 用户数据,一个联合体 };events:事件类型data:用户数据epoll_data_t:用户数据类型C++ 1 2 3 4 5 6 7
typedef union epoll_data { void *ptr; // 指针 int fd; // 文件描述符 uint32_t u32; // 32位无符号整数 uint64_t u64; // 64位无符号整数 } epoll_data_t;
4 信号驱动 IO
TCP
5 异步 IO
前 4 种 IO 模型都是同步 IO,即用户进程发起 IO 请求后,需要等待内核完成 IO 操作后才能继续执行。
异步 IO 模型,用户进程发起 IO 请求后,不需要等待内核完成 IO 操作,用户进程可以继续执行,当内核完成 IO 操作后,会通知用户进程。
| C++ | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
程序运行后一秒,999,888,666 会间隔一秒,依次输出。
而后,t: 1、t: 2、t: 3、t: 4、t: 5 会依次输出。
前者是异步的,而后者是同步的,通过代码不难理解:同步的是 wait(),阻塞;异步的是 async_wait(),不阻塞,任务在 io.run() 之后,由后台处理。
TCP_Server
UDP_Server
| C++ | |
|---|---|
1 | |
errno
| 错误码 | 别名 | 错误描述 | note |
|---|---|---|---|
| 4 | EINTR |
信号中断 | 通常是由于用户按下了 Ctrl + C |
| 9 | 文件描述符无效 | ||
| 11 | EAGAIN EWOULDBLOCK |
操作被阻塞 | 非阻塞下,没有数据可读或写 |
| 98 | 地址已经在使用 | 通常是 bind() 时,地址已经被占用 |
|
| 107 | 传输终点没有连接 |
HTTP
HTTP 协议是基于 TCP 协议的应用层协议,默认端口号是 80(HTTPS是 443),HTTP 协议的通信模型是 请求-响应 模型
- 请求,即客户端向服务端发送的消息
- 响应,即服务端向客户端发送的消息
HTTP 协议的请求消息和响应消息都是由 请求 / 响应行、请求 / 响应头、请求 / 响应体 组成
-
请求行 由三部分组成:请求方法、请求路径、HTTP版本
-
请求方法:
- GET:
- 用途: 请求获取指定资源.不应该对服务器端数据产生任何影响。
-
示例:
GET /index.html,获取首页信息。 -
POST:
- 用途: 用于向指定资源提交数据,请求服务器进行处理。常用于提交表单数据或上传文件。
-
示例:
POST /users,提交用户注册表单。 -
PUT:
- 用途: 请求服务器存储一个资源。通常是更新已存在的资源或创建新资源。
-
示例:
PUT /products/123,更新产品编号为123的商品信息。 -
DELETE:
- 用途: 请求服务器删除指定的资源。
-
示例:
DELETE /users/456,删除用户编号为456的用户信息。 -
HEAD:
- 用途: 请求获取指定资源的响应头信息,而不获取响应体的内容。通常用于检查资源是 否存在或获取资源的元信息。
-
示例:
HEAD /documents/789,检查文档编号为789的资源是否存在。 -
OPTIONS:
- 用途: 请求获取目标资源所支持的通信选项。用于查询服务器支持的HTTP方法。
-
示例:
OPTIONS /products,查询服务器支持的HTTP方法。 -
TRACE:
- 用途: 用于追踪路径。发送请求时,服务器会返回该请求所经过的服务器路径。主要用 于调试和测试。
-
示例:
TRACE /debug,追踪请求的路径。 -
CONNECT:
- 用途: 用于建立与目标资源的隧道连接,通常用于加密连接,如HTTPS。
- 示例:
CONNECT www.example.com:443,与目标服务器建立加密连接。
-
请求路径:
/、/index.html、/jiao.html、...- 我们将请求路径称为
URI,即统一资源标识符,而URL是URI的子集
- 我们将请求路径称为
-
HTTP版本:
HTTP/1.0、HTTP/1.1、HTTP/2.0
-
-
请求头 由请求头字段和请求头字段值组成,每个请求头字段都有特定的含义,常见的请求头字段有:
Accept:指定客户端能够接收的内容类型Accept-Encoding:指定客户端能够接收的内容编码方式Accept-Language:指定客户端能够接收的语言Connection:指定客户端与服务端的连接类型Host:指定请求的主机名和端口号User-Agent:指定客户端的类型Referer:指定请求的来源页面Cookie:指定请求的 CookieContent-Type:指定请求体的类型Content-Length:指定请求体的长度Authorization:指定请求的授权信息If-Modified-Since:指定请求的资源的最后修改时间If-None-Match:指定请求的资源的 ETag 值
- 请求体 具体的数据
- 通常是在 post 请求中,将表单数据放在请求体中
-
响应行 由三部分组成:HTTP版本、状态码、状态码描述
- HTTP版本:
HTTP/1.0、HTTP/1.1、HTTP/2.0 - 状态码:
200、404、500、... - 状态码描述:
OK、Not Found、Internal Server Error、... - 例如:
HTTP/1.1 200 OK
- HTTP版本:
- 响应头 由响应头字段和响应头字段值组成,每个响应头字段都有特定的含义,常见的响应头字段有:
Content-Type:指定响应体的类型Content-Length:指定响应体的长度Content-Encoding:指定响应体的编码方式Content-Language:指定响应体的语言Content-Disposition:指定响应体的处理方式Set-Cookie:指定响应的 CookieLocation:指定响应的重定向地址Last-Modified:指定响应的资源的最后修改时间ETag:指定响应的资源的 ETag 值
- 响应体 具体的数据
- 例如网页的 HTML 代码
- 例如图片的二进制数据、pdf文件等