实践项目
实现一个 telnet 版本的聊天服务器,主要有以下需求。
- 每个客户端可以用使用
telnet ip:port
的方式连接到服务器上。 - 新连接需要用用户名和密码登录,如果没有,则需要注册一个。
- 然后可以选择一个聊天室加入聊天。
- 管理员有权创建或删除聊天室,普通人员只有加入、退出、查询聊天室的权力。
- 聊天室需要有人数限制,每个人发出来的话,其它所有的人都要能看得到。
实现一个简单的 HTTP 服务器,主要有以下需求。
- 解释浏览器传来的 HTTP 协议,只需要处理 URL path。
- 然后把所代理的目录列出来。
- 在浏览器上可以浏览目录里的文件和下级目录。
- 如果点击文件,则把文件打开传给浏览器(浏览器能够自动显示图片、PDF,或 HTML、CSS、JavaScript 以及文本文件)。
- 如果点击子目录,则进入到子目录中,并把子目录中的文件列出来。
实现一个生产者 / 消费者消息队列服务,主要有以下需求。
- 消息队列采用一个 Ring-buffer 的数据结构。
- 可以有多个 topic 供生产者写入消息及消费者取出消息。
- 需要支持多个生产者并发写。
- 需要支持多个消费者消费消息(只要有一个消费者成功处理消息就可以删除消息)。
- 消息队列要做到不丢数据(要把消息持久化下来)。
- 能做到性能很高。
v2ray 文档 https://www.v2ray.com/developer/intro/roadmap.html
自己实现一个 socks5
Python: https://hatboy.github.io/2018/04/28/Python%E7%BC%96%E5%86%99socks5%E6%9C%8D%E5%8A%A1%E5%99%A8
概述
早起 TCP/IP 被移植到 UNIX 平台时,设计者们希望像访问文件一样去访问网络。
Linux 提供了三种类型套接口:
- 流式套接口(SOCK_STREAM)
提供了可靠的双向顺序数据流连接。
- 数据报套接口(SOCK_DGRAM)
提供双向的数据传输。
- 原始套接口(SOCK_RAW)
这种套接口允许进程直接存取下层的协议。
现在全世界的人都在解决 C10K 问题。
http://www.kegel.com/c10k.html
翻译版:https://www.oschina.net/translate/c10k
问题
TCP 与 UDP 的异同?
面向连接的可靠传输,有重传机制,还有流量控制、拥塞控制
面向无连接的不可靠传输,以带数据边界的数据报形式传送,速度快
TCP 连接的特点和基本流程,何谓“三次握手”?
A = seq1 => B
B = seq1 + 1 / seq2=> A
A = seq2 + 1 => B
简述 OSI 七层模型和 TCP/IP 模型的异同
OSI 参考模型只是一个抽象的理论模型,TCP/IP 是实际网络应用中的经验产物,它与 OSI 的四层相关联。
二倍 msl(maximum segment lifetime) 作用?
结合四次挥手可以很清晰的看到:
- 可靠的实现 TCP 全双工连接的终止(保证最后一次 ack 到达)
- 允许老的重复报文在网络中的消逝(旧报文到达导致重复建立连接)
简述 C/S 的运行模型。
即客户端与服务端运行模型,服务端为客户端提供服务,一直等待客户请求;
客户端向服务端发出请求,并等待响应结果。
TCP
listen 监听列表满了怎么办?
TCP 将忽略客户传来的 SYN 分节,不发送 RST,客户端将重发 SYN。
在调用 select 函数时,如何使得进程跳出阻塞状态?
设置信号处理函数、直接指定时间?
shutdown、close 区别?
close:将套接口描述字引用计数器减一,计数器为零套接口才会关闭,并且终止了读写两个方向。
shutdown:不管引用计数器为何值,直接终止网络连接,可单独指定终止读、写。
网络编程时,为什么要考虑字节顺序问题?
因为网络字节序与主机字节序不一致。
编程实现:TCP,客户机产生两个随机数,发给两个服务器,A将两数相加,B相减,分别将结果返给客户机。
简述 socket API 的差别:
send、write、writev、sendto、sendmsg
什么是多路复用 select,给出一个典型应用
内核发现进程指定的一个或多个I/O条件就绪,就通知进程。典型:多个描述字多路复用,比如交互式输入和网络套接字。
出现粘包如何处理?
UDP与原始套接口
UDP协议中发送数据大于缓冲区大小,系统如何处理,说明理由。
UDP将直接丢弃这个数据报,并且不发送任何报错信息。
UDP协议与TCP协议的服务器在处理客户端请求时有何异同?
UDP采用循环服务器的工作方式,它仅有的单个套接口用于接收所有到达的数据报,并发回所有的响应,UDP套接口有一个接收缓冲区用于存放到来的数据报。
与 TCP 中的 connect 有何差别?
UDP 不需要建立连接,使用 connect 只是记录目的方的IP与端口,调用后,可直接
read
、write
。而且这里的 read 将不会受到来自其他主机的应答。
如何避免UDP协议下客户端将非服务端发送的应答,误认为是服务器应答?
- 通过 recvfrom 里返回的 IP 与端口区分
- 使用 connect
简述ping程序的功能与实现原理。
利用原始套接口发送 icmp 回射请求,等待对方的应答,应答中包含请求的标识符、序列号、时间戳
简述traceroute程序的功能与实现原理。
首先发送 ttl 为1的 udp 数据报,然后逐次递增ttl,确定下一跳的路由。
当 icmp 报文到达目标主机时,目标主机返送一个 icmp 错误,显示端口不可达。
带外数据
什么是带外数据?TCP 协议支持多少个字节的带外数据?
若连接的某端发生了重要的事情,希望迅速通知对端,这种通知要在发送缓存数据前发送。
带外数据并不要求在客户与服务器间再使用一个连接,而是映射到已有的连接中。
只支持一个字节
试给出一个使用带外数据提供的服务。
心搏函数。
TCP 有没有为紧急数据提供单独的数据信道,它是如何实现带外数据传输的?
TCP 没有单独的通道,而是使用的紧急模式实现的。
TCP 发送和接收带外数据有哪些方法?
send(sockfd, 'A', 1, MSG_OOB)
SIGURG 信号处理函数
- select 异常集合接收
- 带外标志读取
TCP 协议收到一个新的紧急指针时,将通知接收进程,有哪些通知方法?
SIGURG 信号、select
如果进程设置了 SO_OOBINLINE 选项,能否通过设置 MSG_OOB 标志来读取带外数据?为什么?应该采用什么方式读取带外数据?
不能,SO_OOBINLINE 选项表示将紧急数据留到普通的套接口缓冲区,所以正常的 read 就行了。
可通过 sockatmark 读取带外标识位置。
阻塞与非阻塞
为什么会阻塞?
两个缓冲区:内核缓冲区、进程缓冲区,当内核缓冲区未满足时,该进程将被投入休眠。
什么是非阻塞?
将一个套接口设为非阻塞 => 通知内核,当所请求的 I/O 操作未满足时,不要阻塞该进程,而是返回一个错误
优点:当 I/O 操作不能立即完成时,进程还可以继续后续的操作,提高自身运行效率。
缺点:进程一直处于运行状态,可能占用大量CPU时间,影响其他进程的运行效率。
非阻塞
非阻塞connect三个用途
- 完成connect需要花一个RTT时间,局域网的几毫秒到广域网的几秒。此期间可以将三次握手迭合在其他处理上
- 利用非阻塞 connect 技术同时建立多个连接
- 利用 select 指定时间限制,缩短connect的超时(很多实现中connect超时为75秒到数分钟)
1.设置套接口为非阻塞 |
I/O 复用
可等待多个描述字的就绪
信号驱动
内核在描述字就绪时,发送 SIGIO 信号通知进程
绑定信号以及对应的处理函数 => 继续执行其他操作 => 满足后自动处理
异步
告知内核启动某个操作,并让内核在整个操作完成(包括将数据从内核拷贝到进程缓冲区里)后通知
与信号驱动的区别:
- 信号驱动:由内核通知何时可以启动一个 I/O 操作
- 异步:由内核通知 I/O 何时完成
aio_read
给内核传递描述字、缓冲区指针、缓冲区大小、文件偏移,并告诉内核当操作完成时如何通知进程。
问题
哪种情况下适合采用阻塞式I/O编程?
访问一个或多个服务进程时,各访问之间有顺序关系
非阻塞与阻塞在 CPU 利用率上有什么区别
阻塞期间不占用 CPU 时间,不影响其他进程的工作效率,进程可能长时间处于休眠,在此期间进程不能执行别的任务,进程自身的效率不高。
非阻塞,进程还可以执行后续的任务,提高自身的工作效率,进程一直处于执行期间,可能占用大量CPU时间来检测IO操作是否完成,影响其他进程的执行效率。
哪些套接口会发生阻塞
// 数据发送 发送缓冲区没有空间
sendmsg, sendto, send, write, writev
// 数据接收,接收缓冲区没有空间
recvmsg, recvfrom, recv, read, readv
// 完成三次握手
connect
// 无新连接到达
accept
广播
用途
局限于局域网内使用
资源发现
在本地子网中定位一个服务器主机,寻找其 IP 地址(例如 ARP、DHCP)
节约带宽
在有多个客户机与单个服务器机通信的局域网环境中尽量减少分组流量。
多播
用途
局域网、跨广域网都可使用
问题
与广播的区别,以及分别的应用场景
- 广播是向网络中所有主机发送信息
- 广播由于是向全网发,其他无关主机都会收到,而且要到传输层才能处理,浪费网络、计算资源
- 广播应用实例:网络时间协议
进程间通信
常见信号及其默认动作
SIGABRT 异常终止(abort) 终止 |
管道与 FIFO
- 管道可用于具有亲缘关系进程间的通信
- 命令管道克服了管道没有名字的限制,命名管道允许无亲缘关系进程间的通信
UNIX 域协议
IPC
消息通信
消息通信通过消息队列实现进程通信
- 消息队列是消息的链接表
- 有足够的权限的进程可以向队列中添加消息,被赋予读权限的进程可以读取队列中的消息
- 消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等特点
- 消息队列不需要进程间具有亲缘关系
信号与信号量
- 用于通知接受进程有某事件发生
- 进程可以发送信号给进程本身
- 信号 => 信号量,能使用多次?
共享内存
进程能够不涉及内核而访问其中的数据
- 使用多个进程可以访问同一块内存空间,是单机最快的可用 IPC 形式
- 针对其他通信机制运行效率较低而设计的,往往与其他通信机制结合来达到进程间的同步和互斥,如信号量
问题
- 命名管道、管道的区别
- 命名管道以 FIFO 的形式存在于文件系统中,与 FIFO 创建进程无亲缘关系的进程只要能访问该路径,就能彼此通信
- 管道在最后一个关闭后自动消失,而 FIFO 需要通过 unlink 删除
并发
多进程
多线程
IO 多路复用
异步I/O模型的发展技术是: select -> poll -> epoll -> aio -> libevent -> libuv。Unix/Linux用了好几十年走过这些技术的变迁,然而,都不如 Windows I/O Completion Port 设计得好(免责声明:这个观点纯属个人观点。相信你仔细研究这些I/O模型后,你会得到你自己的判断)。
一个线程维护多个 Socket
由于 socket 是文件描述符,因而某个线程盯的所有的 socket,都放在一个文件描述符集合 fd_set 中,这就是项目进度强,然后调用 select 函数来监听文件描述符集合是否有变化。一旦有变化,就会依次查看每个文件描述符。那些发生变化的文件描述符在 fd_set 中对应的位都设为 1,表示 socket 可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯下一轮的变化。
从“派人盯着”到”有事通知“
上面的方式在文件描述符有变化时,都会采用轮询的方式确定具体是哪个 socket 有变化,也就是需要将全部项目都过一遍的方式来查看进度,这就大大影响了一个项目组能够支撑的最大的项目数量。因而使用 select,能够同时盯的项目数量由 FD_SETSIZE 限制。
如果改成事件通知的方式,情况就会好很多。(select 里不能返回具体是哪个 socket 变化了?)
最终方式:epoll + callback
AIO: Asynchronous IO ,异步非阻塞
BIO:Block-io,同步且阻塞式IO
NIO:Non-block IO,同步非阻塞
API
杂项
int to char array
sprintf(buf, "%d", num); |
端口复用(注意放到 bind 前面)
int opt = 1; |
地址
#include <linux/sock.h>
// 通用型套接字地址结构 |
字节操纵
|
IP 地址转换
|
创建
>
>
>
socket
int socket(int domain, int type, int protocol); |
bind
将套接口指定IP、port,可两者都指定,也可都不指定;
服务端通常在启动时绑上端口;
客户端通常不绑定端口,由内核分配临时端口;
可通过
getsockname
来返回协议地址。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen); |
listen
监听本地地址和端口
套接口状态:closed => listen
sockfd-已绑定的socket描述符
backlog-已完成连接、等待接收的队列长度,LISTENQ?
int listen(int sockfd, int backlog); |
accept
当服务请求到达 accept 监视的 socket(监听套接口)时,系统将自动建立一个新的 socket(已连接套接口),并将此 socket 和客户进程连接起来。
int accept(int sockfd, sockaddr* cliaddr, int *addrlen); |
收发
#include <unistd.h>
read
从套接口接收缓冲区中读取len字节的数据,成功返回,返回值是实际读取数据的字节数
ssize_t read(int fd, void *buf, size_t count); |
write
从套接口中发送 len 字节的数据,成功返回,返回实际写入数据的字节数
ssize_t write(int fd, const void *buf, size_t count); |
recv
int recv(int fd, void *buf, int len, unsigned int flags); |
recvfrom
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, |
send
int send(int fd, const void *msg, int len, unsigned int flags); |
sendto
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, |
recvmsg
sendmsg
连接
connect
TCP 客户端与服务器建立连接用 connect 函数
int connect(int sockfd, struct sockaddr * addressp, int addrlen); |
关闭
shutdown
终止网络连接并停止所有信息的发送与接收(不管引用计数器为何值)
|
close
计数器减一,不会完全关闭
参数
getsockopt/setsockopt |
地址
gethostbyaddr getaddrbyhost,... |
非阻塞
int flag = fcntl(sockfd, F_GETFL, 0); |
复用
fd_set
void FD_ZERO(fd_set * fdset); // 清除描述字集 fdset 中的所有位 |
select
|
poll
|
epoll
//创建 epoll |
信号
static void sig_alrm(int signo) { |