网络编程

本指南深入探讨网络编程的复杂性、涵盖协议、TCP/UDP套接字、并发等内容

关键概念

  • 网络协议 Networking Protocols
  • TCP (Transmission Control Protocol): 确保可靠的数据传输
  • UDP (User Datagram Protocol): 速度更快,但不能保证数据的可靠传输
  • 套接字 Sockets
  • TCP Sockets: 用于面向连接的通信
  • UDP Sockets: 用于无连接通信

TCP/IP 网络模型

同一台设备进程间通信: 管道、消息队列、共享内存、信号

不同设备进程间通信: 需要网络通信,为了兼容多种多样的设备,需要一套通用的网络协议

应用层 (Application Layer)

应用层只需要专注用户提供应用功能,比如HTTP、FTP、DNS、SMTP, 应用层是不需要关心数据是如何传输的,以及应用层工作在操作系统中的用户态,传输层及以下则工作在内核态

传输层 (Transport Layer)

应用层的数据包会传输给传输层,传输层是为应用层提供网络支持, 传输层的两个传输协议,分别是TCP(Transmission Control Protocol)和UDP.

TCP-传输控制协议,TCP相比UDP多了很多特性,比如流量控制、超时重传、拥塞控制等,这些都是为了保证数据包能可靠地传输给对方.

UDP-相对简单,只负责发送数据包,不保证数据包是否能抵达对方,但实时性相对更好,传输效率更高

当数据包大小超过MSS(TCP最大报文段长度)就要将数据包分块.这样即使中途有一个分块丢失或损坏,只需要重新发送这个分块,而不用重新发送整个数据包. TCP协议中,每个分块称为一个TCP段(TCP Segment).

网络层 (Internet Layer)

网络层最常使用的是IP协议(Internet Protocol),IP协议会将传输层的报文作为数据部分,再加上IP包头组装成IP报文,如果IP报文大小超过MTU(以太网中一般为1500bytes)就会再次分片.

IP报文-TCP报文

使用IP地址给设备进行编号,IP地址分成以下两种:

  • 网络号: 负责标识IP地址是属于那个【子网】的
  • 主机号: 负责标识同一[子网]的不同主机
1
2
3
4
5
6
10.100.122.0/24, /24表示 子网掩码 `255.255.255.0`.

将IP地址与子网掩吗进行按位运算得到网络号:10.100.122.0
将IP地址与取反的子网掩码进行按位运算,就可以得到主机号

在寻址的过程中,先匹配到相同的网络号(表示要找到同一个子网),才会去找对应的主机

网络接口层在IP头部的前面加上MAC头部,并封装成数据帧(Data Frame)发送到网络上, MAC头部是以以太网使用的头部,它包含了接收方和发送方的MAC地址等信息,通过ARP协议获取对方的MAC地址.

网络接口层主要为网络层提供[链路级别]传输的服务,负责在以太网,WI-FI这样的底层网络上发送原始数据包,工作在网卡这个层次,使用MAC地址来标识网络上的设备.

TCP IP Four Layer

每一层的封装格式: 网络接口层的传输单位是帧(frame), IP层的传输单位是包(packet),TCP层的传输单位是段(segment),HTTP的传输单位则是消息(message)

帧 包 段 消息

输入网址到网页显示,期间发生了什么?

HTTP

首先浏览器需要对URL进行解析,从而生成发送给Web服务器的请求信息

1
2
3
4
5
6
7
URL元素组成:

http + // + web服务器 + /+目录名+

http: 表示访问数据的协议(http,https,ftp)
//后面的字符串标识服务器的名称
/+目录名 : 表示数据源的路径名

URL实际上是请求服务器里的文件资源

对URL进行解析后,浏览器确定Web服务器和文件名,接下来就是根据这些信息来生成HTTP请求消息

HTTP数据包

DNS

通过浏览器解析URL并生成HTTP消息后,需要委托操作系统将消息发送给Web服务器,在发送之前需要完成查询服务器域名对应的IP地址

1
2
3
4
5
6
7
8
9
DNS服务器,专门负责Web服务器域名和IP对应关系

`www.chyidl.com`,在域名中,越靠右的位置表示层级越高,实际域名最后还有一个点, `www.chyidl.com.`.这个最后一个点代表根域名.

所有域名的层级关系类似一个树状结构:

* 根DNS服务器(.)
* 顶级域DNS服务器(.com)
* 权威DNS服务器(server.com)

根域名DNS服务器信息保存在所有互联网DNS服务器中.客户端只要能够找到任意一台DNS服务器,就可以通过它找到根域名DNS服务器.

  • 域名解析的工作流程
1
2
3
4
5
6
1. 客户端发起DNS请求,询问www.server.com的IP.并发给本地DNS服务器(就是客户端TCP/IP设置填写的DNS服务器地址)A
2. 本地域名服务器收到客户端请求后,首先查找缓存www.server.com,找到即返回IP地址,否则询问根域名服务器
3. 根域名DNS收到本地DNS的请求后,发现后置是.com,返回给本地DNS`.com`顶级域名的服务器地址
4. 本地DNS收到顶级域名服务器地址后,向`.com`顶级域名发起请求查询`www.server.com`的IP地址,顶级域名服务器返回`server.com`权威DNS服务器地址
5. 本地DNS向权威DNS服务器发送请求,权威DNS服务器查询后将对应的IP地址返回给本地DNS
6. 本地DNS再将查询到的IP地址返回给客户端,客户端与目标建立连接

DNS域名解析结果缓存

浏览器首先看有没有对该域名的缓存,没有就去查看操作系统有没有该域名缓存,没有再去查看本地系统的hosts文件,没有再去问[本地DNS服务器]

协议栈

协议栈

应用程序(浏览器)通过调用Socket库,来委托协议栈工作,协议栈的上半部分有两个部分,分别是负责收发数据的TCPUDP协议,这两个传输协议会接收应用层的委托收发数据

协议栈的下面一半是用于IP协议控制网络包收发操作,在互联网上传数据时,数据会被切分一块块网络包.将网络包发送给对方的操作由IP协议负责,ICMP协议用于告知网络包传送过程中产生的错误以及各种控制信息. ARP协议用于根据IP地址查询响应的以太网MAC地址.

TCP - 可靠传输

  • TCP报文头部格式

TCP报头格式

1
2
3
4
5
6
7
8
9
`源端口` `目标端口`: 区分应用, 由于是16位,2^16 = 65536, 源端口(通常是随机生成的)
`序号`: 解决包乱序的问题 2^32 = 4292967296
`确认号`: 目的是确认发出去对方是否有收到,如果没有收到就应该重新发送,直到送达,这是为了解决丢包问题
`状态位`: 更改状态
`SYN`: 发起连接
`ACK`: 回复
`RST`: 重新连接
`FIN`: 结束连接
`窗口大小`: TCP要做流量控制,通信双方声明窗口(缓存大小),标识自己当前能够的处理能力

TCP三次握手

1
2
3
4
5
1. 开始,客户端和服务端都处于`CLOSED`状态,服务端主动监听某个端口,处于`LISTEN`状态
2. 客户端主动发起连接`SYN`,之后客户端处于`SYN_SENT`状态
3. 服务端接收连接,返回`SYN`,并且`ACK`客户端的`SYN`,之后处于`SYN-RCVD`状态
4. 客户端收到服务端发送的`SYN`和`ACK`之后,发送对`SYN`的确认`ACK`,之后处于`ESTABLISHED`状态,
5. 服务端收到`ACK`之后,处于`ESTABLISHED`状态

三次握手目的是保证双方都有发送和接收的能力

TCP的连接状态查看在Linux可以通过netstat -napt命令查看

1
2
3
4
5
6
7
8
[~]$ netstat -anpt
(Not all processes could be identified, non-owned process info
will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN -
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN -
tcp 0 324 192.168.100.5:22 192.168.100.4:43028 ESTABLISHED -

TCP拆包

MTU: 一个网络报的最大长度,以太网中一般为1500字节
MSS: 除去IP和TCP头部之后,一个网络报所能容纳的TCP数据的最大长度

IP

TCP在执行连接、收发、断开等各各阶段操作时,都需要委托IP模块将数据封装成网络包发送给通信对象

ip包格式

源地址IP: 客户端输出IP地址
目标地址: 通过DNS域名解析得到的Web服务IP

Linux操作系统可以使用route -n命令查看当前系统的路由表

1
2
3
4
5
6
7
[~]$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.50.1 0.0.0.0 UG 600 0 0 wlan0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
192.168.50.0 0.0.0.0 255.255.255.0 U 600 0 0 wlan0
192.168.100.0 0.0.0.0 255.255.255.0 U 0 0 0 nebula1

MAC

生成IP头部之后,接下来网络包还需要在IP头部的前面加上MAC头部.

在Mac包需要发送方MAC地址接收方目标MAC地址,用于两点之间的传输。一般在TCP/IP通信中,MAC包头的协议类型只使用:

1
2
0800: IP 协议
0806: ARP 协议
  • ARP 协议

    ARP协议会在以太网中以广播的形式,

使用arp -a 查看ARP缓存内容

1
2
3
4
5
6
7
8
9
10
11
[~]$ arp -a
? (192.168.50.129) at <incomplete> on wlan0
? (169.254.226.14) at 50:81:40:e8:59:1e [ether] on wlan0
3BP (192.168.50.194) at b8:27:eb:06:7e:4e [ether] on wlan0
aapc (192.168.50.71) at 84:39:be:67:05:a6 [ether] on wlan0
RT-AC86U-40B0 (192.168.50.1) at f0:2f:74:39:40:b0 [ether] on wlan0
Chyis-Air (192.168.50.171) at 1c:57:dc:3d:6b:b7 [ether] on wlan0
? (192.168.50.73) at 98:9e:63:45:e9:88 [ether] on wlan0
? (192.168.50.215) at ee:de:39:21:3d:da [ether] on wlan0
? (192.168.50.216) at 1c:d6:be:86:dc:60 [ether] on wlan0
? (192.168.50.50) at 68:54:fd:8a:68:89 [ether] on wlan0

网络包

网卡

网络包只是存放在内存中的一串二进制数字信息,没有办法直接发送给对方,因此需要将数字信息转换为电信号,负责执行这一操作的时网卡,要控制网卡还需要靠网卡驱动程序.

数据包

网卡驱动获取网络包之后,会将其复制到网卡内的缓存中,接着会在其开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列.

交换机

交换机设计是将网络包原样转发到目的地,交换机工作在MAC层,称为二层网络设备.交换机接收所有包并存放在缓冲区后,查询这个包的接收方MAC地址是否MAC地址表中有记录. 交换机的MAC地址主要包含两个信息:

1
2
1. 一个是设备的MAC地址
2. 另一个是该设备连接在交换机的那个端口上

交换机根据MAC地址表查找MAC地址,然后将信号发送到相应的端口.

路由器

路由器是基于IP设计的,俗称三层网络设备,路由器的各个端口都具有MAC地址和IP地址

交换机是基于以太网设计的,俗称二层网络设备,交换机的端口不具有MAC地址

HTTP

HTTP基本概念

HTTP (HyperText Transfer Protocol) 超文本传输协议

  • HTTP常见的状态吗?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    1xx: 提示信息,表示目前是协议处理的中间状态,还需要后续的操作
    2xx: 成功,报文已经收到并正确的处理
    200 OK: 表示一切正常
    204 No Content: 响应头没有body数据
    206 Partial Content: 应用于HTTP分块下载或断点续传,表示响应返回的body数据并不是资源的全部,而是其中的一部分,也是服务器处理成功的状态
    3xx: 重定向,资源位置发生变动,需要客户端重新发送请求
    301 Moved Permanently: 永久重定向,说明请求的资源已经不存在,需改进新的URL再次访问
    302 Found: 表示临时重定向,说明请求的资源还在,但暂时需要用另一个URL来访问
    301,302都会在响应头里使用字段Location,指明后续要跳转的URL,浏览器会自动重定向新的URL
    304 Not Modified: 表示资源为修改,重定向已经存在的缓冲文件
    4xx: 客户端错误,请求报文有误,服务器无法处理
    400 Bad Request: 表示客户端请求的报文有错误
    403 Forbidden: 表示服务器禁止访问资源
    404 Not Found: 表示请求的资源在服务器上不存在
    5xx: 服务器错误,服务器在处理请求时内部发生了错误
    500 Internal Server Error: 服务器错误(笼统通用的错误码)
    501 Not Implemented: 表示客户端请求的功能还不支持
    502 Bad Gateway: 通常是服务器作为网管或者代理时返回的错误码,表示服务器自身工作正常,访问后段服务器发生错误
    503 Service Unavailable: 表示服务器当前很忙,暂时无法响应客户端
  • HTTP常见字段?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
* Host 字段
客户端发送请求时,用来指定服务器的域名

* Content-Length 字段
服务器返回数据时,表明本次回应的数据长度

HTTP协议通过设置回车符,换行符作为HTTP header的边界,通过Content-Length字段作为HTTP body的边界,这两个方式为了解决“粘包”的问题

* Connection 字段
用于客户端要求服务器使用HTTP长连接, 开启HTTP Keep-Alive机制后,连接就不会中断,而是保持连接,当客户端发送另一个请求时,他会使用同一个连接,一直持续到客户端或服务端提出断开连接

* Content-Type 字段
Content-Type用于服务器回应时,告诉客户端,本次数据是什么格式.

* Accpet字段
客户端请求时表示自己可以接收的数据格式

* Content-Encoding 字段
数据的压缩方法,表示服务器返回的数据使用什么压缩格式

* Accept-Encoding 字段
客户端可以接收那些压缩方法
  • GET与POST
1
2
3
4
5
6
7
8
9
* GET:
从服务器获取指定资源, GET请求的参数位置一般写在URL中,URL规定只能支持ASCII,所以GET请求的参数只允许ASCII字符

* POST:
POST请求携带数据一般写在Body中

* 安全和幂等
GET方法就是安全且幂等的,因为他只读操作, 可以对GET请求的数据做缓存,
POST方法是新增或提交数据,会修改服务器上的资源,是不安全的,且多次提交会创建多个资源,所以不是幂等的
  • HTTP缓存技术
1

GET与POST

HTTP特性

HTTP缓存技术

HTTPS与HTTP

HTTP/1.1 HTTP/2 HTTP/3 演变

Apache vs Nginx

Nginx采用事件驱动架构,可在一个线程中处理多个请求,相比之下,Apache会为每个请求创建一个线程,由于Nginx采用了”多请求-单线程”设计,Nginx可在用户请求量增加时保持持续的响应性能,在某些评估中,Nginx的性能比Apache高出2.5倍,而资源消耗却与Apache类似.

网上关于Apache和Nginx性能比较的文章非常多,基本有如下定论:

    1. Nginx在并发性能上比Apache强很多,如果时纯静态资源(图片、js、css)那么Nginx时不二之选
    1. Apache有mod_php,在PHP类的应用场景下比Nginx部署起来简单很多,一些老的PHP项目用Apache来配置运行非常简单,例如Wordpress
    1. Nginx的模块比较容易写,可以通过C的mod实现接口性质的服务,并且有用惊人的性能,分支OpenRestry可以配合Lua来实现很多自定义功能,兼顾扩展性和性能.

网络编程的历史

  • 最原始的网络编程伪代码

    1
    2
    3
    4
    5
    6
    listen(port)    # 监听在接收服务的端口上
    while True: # 一直循环
    conn = accept() # 接收链接
    read_content = read(conn) # 读取连接发送过来的请求
    response = process(conn) # 执行业务逻辑,并得到客户端回应的内容
    conn.write(response) # 将回应写回给连接

    最原始的Linux中accept,read,write调用都是阻塞,这就导致以上代码只能同时处理一个连接.

  • 每个连接开一个进程

1
2
3
4
5
6
7
8
listen(port) # 监听在接收服务的端口上
while True:
conn = accept() # 接收连接
if fork() == 0:
# 子进程
read_content = read(conn) # 读取连接发送过来的请求
response = process(conn) # 执行业务逻辑,并得到客户端回应的内容
conn.write(response) # 将回应写回给连接

使用子进程来处理连接,父进程继续等待连接进来,但这种方式有如下两个明显的缺陷:

    1. fork()调用比较费时,需要对进程进行内存拷贝,即使现在Linux普遍引入COW(Copy on Write)技术(fork的时候不做内存拷贝,只有其中一个副本发生Write的时候才进行copy)加速了fork()的效率,但fork依旧是个比较”重的系统调用”
    1. 较多的内存占用,也是由于上述的内存复制造成的
  • 每个连接开线程

  • 引入进程/线程池

计算机领域“空间换时间”,即用使用更多内存的方式换取更快的运行速度.事先创建出很多进程/线程,就像一个池子,这样虽然会浪费一部分的内存,但连接过来的时候就省去开启进程/线程的时间.

但这种方式会有一个缺陷:当并发数大于进程/线程池的大小的时候,性能就会发生很大的下滑:

非阻塞网络编程

  • select() vs poll()

select()是在1983年首次出现在4.2版的BSD Unix中,poll()出现的稍微晚,1997年在Linux内核2.1.23版本中加入. select()只能处理小于等于1024的文件描述符.

  • select:
1
2
3
4
#include <sys/select.h>

int
select(int nfds, fd_set * restruct readfds, fd_set * restruct writefds, fd_set * restrict errorfds, struct timeval * restrict timeout);
  • poll
1
2
3
4
#include <poll.h>

int
poll(struct pollfd fds[], nfds_t nfds, int timeout);
  • epoll()

epoll是Linux内核的可扩展I/O事件通知机制,它设计目的只在取代既有POSIX select与poll系统函数.让需要大量操作文件描述符的程序发挥更优异的性能(select,poll系统函数花费的事件复杂度为O(n),epoll耗时O(1)). epoll与FreeBSD的kqueue类似,底层都是由可配置的操作系统内核对象构建,并以文件描述符形式呈现用户空间.

1
2
3
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event * event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

事件通知API

libevent是一个一步事件处理软件函式库,libevent提供了一组应用程序编程接口(API),让程序员可以设定某些事件发生时所执行的函式。libevent可以取代网络服务器使用的事件循环检测框架. 按照libevent的官方网站,libevent库提供一下功能,当一个文件描述符的特定事件(可读、可写、或出错)发生时,或一个定时事件发生时,libevent就会自动执行用户指定的回调函数,来处理事件.

一个基于libevent网络server,这有助于event-driven programming(事件驱动编程)

  • 水平触发LT(level-triggered) & 边沿触发ET(edge-triggered)
1
2
3
4
LT是一个持续的状态,ET是个事件性的一次性状态.

Level Triggered模式下只要某个readable/writeable状态,无论什么时候进行epoll_wait都会返回该socket.
Edge Triggered模式下只有某个socket从unreadable变为readable或从unwriteable变为writable时,epoll_wait才会返回该socket.

TCP socket 安全问题

  • 在Internet环境下,安全问题分为如下几类:
    1. 信息传输过程中被黑客窃取
    1. 服务器自身的安全
    1. 服务端数据的安全

首先,如果能用https,就尽量用https,能用nginx等常见服务器,就用常见服务器,主要能避免以下问题:

  1. 自己实现的协议&Server端可能会有各种Bug,被缓冲区溢出攻击等
  2. SSL加密体系在防监听方面已经足够成熟,值得信赖

工程实现过程中,要考虑:

  1. 各种可能的缓冲区溢出攻击
  2. SYN flood攻击,慢连接攻击
  3. DDoS防起来有难度,但至少能防御DoS攻击

业务逻辑层面,要考虑:

  1. 每个接口都要做好用户&权限验证
  2. 接口会不会被乱用,重放攻击
  3. 攻击方会不会找到一个比较消耗服务端资源的接口,用很小的代价耗尽服务端资源
  4. 你的服务会不会被黑客利用去攻击别的服务,特别是会根据用户输入抓取什么资源的服务
  5. 古老的SQL注入
  6. 无耻的仿冒服务,DNS欺诈
  7. 涉及HTML的,还要考虑跨站…

长连接&连接池

TCP是基于连接的协议,其实这个”连接”只是一个逻辑上的概念,在IP层看来,TCP和UDP仅仅是内容上稍有差别而已.TCP协议仅仅是连接的两端对于四元组和sequence号的一种约定而已.

1
2
3
四元组: 源IP地址、目的IP地址、源端口、目的端口
五元组: 源IP地址、目的IP地址、协议、源端口、目的端口
七元组: 源IP地址、目的IP地址、协议、源端口、目的端口、服务类型、接口索引
  • HTTP长连接

HTTP长连接,HTTP持久连接(HTTP persistent connection, 也称为HTTP keep-alive或HTTP connection reuse)是使用同一个TCP连接发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方式.

tcp connection

  • Keep-Alive的优势
  1. 较少的CPU和内存的使用(由于同时打开的连接的减少)
  2. 允许请求和应答的HTTP管线化
  3. 减少后续请求的延迟(无需再进行握手)
  4. 报告错误无需关闭TCP连接
  • Keep-Alive的劣势
  1. 对于单个文件被不断请求的服务,Keep-Alive可能会极大的影响性能,因为它在文件被请求之后还保持不必要的连接很长时间.
  • 动静分离

对于静态资源的请求,HTTP请求头里的Cookie等信息没有用处,反而占用了宝贵的上行网络资源,用独立的域名存放静态资源后,请求静态资源域名就不会默认带上主域的Cookie,从而解决这个问题

Go 网络编程


网络编程
https://blog.chyidl.com/2024/01/24/网络编程/
作者
Yaqing Chyi
发布于
2024年1月24日
许可协议