我们假定读者掌握了:
- Linux环境下C/C++的系统编程和基本的socket编程方法
- 操作系统基本概念以及Linux的基本概念和原理
- Linux进程和线程的内存地址空间布局和资源关系
我们谈什么,不谈什么:
- Linux下的网络程序设计所遵循的规范
- Linux网络程序的工作模型和原理
- 一般性质上的网络协议设计方法和原则
- 基本上只针对Linux,基本不涉及Windows
- 只涉及TCP协议的通信,不谈UDP
可以先行阅读的参考资料:
进程眼中的线性地址空间
线程眼中的线性地址空间
Linux线程的前世今生
聊聊内存管理
Linux系统调用
goroutine背后的系统知识
从基本socket函数开始
留意一些socket函数与众不同的参数细节
1 2 3 4 5 |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
TCP三次握手在socket接口的位置
强调TIME_WAIT状态
MSL(最大分段生存期)指明TCP报文在Internet上最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值。RFC1122建议是2分钟。
TIME_WAIT 状态最大保持时间是2 * MSL,也就是1-4分钟。
1 |
int listen(int sockfd, int backlog); |
Linux内核协议栈
TCP 的发送
TCP的接收
协议栈完整的收发流程
关于socket接口与内核协议栈的挂接
TCP相关参数的设置方法
套接字设置
1 2 3 4 5 6 7 |
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,
void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
|
SO_REUSEADDR
当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。这个选项允许同一port上启动同一服务器的多个实例(多个进程)。但每个实例绑定的IP地址是不能相同的。(多块网卡的应用场合)
SO_RECVBUF / SO_SNDBUF
发送和接收缓冲区大小,不详述。
TCP_NODELAY / TCP_CHORK
是否采用Nagle算法把较小的包组装为更大的帧。HTTP服务器经常使用TCP_NODELAY关闭该算法。相关的还有TCP_CORK。
TCP_DEFER_ACCEPT
推迟accept,实际上是当接收到第一个数据之后,才会创建连接。(对于像HTTP等非交互式的服务器,这个很有意义,可以用来防御空连接攻击。)
TCP_KEEPCNT / TCP_KEEPIDLE / TCP_KEEPINTVL
如果一方已经关闭或异常终止连接,而另一方却不知道,我们将这样的TCP连接称为半打开的。TCP通过保活定时器(KeepAlive)来检测半打开连接。设置SO_KEEPALIVE选项来开启KEEPALIVE,然后通过TCP_KEEPIDLE、TCP_KEEPINTVL和TCP_KEEPCNT设置keepalive的开始时间、间隔、次数等参数。
保活时间:keepalive_time = TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT
从TCP_KEEPIDLE 时间开始,向对端发送一个探测信息,然后每过TCP_KEEPINTVL 发送一次探测信息。如果在保活时间内,就算检测不到对端了,仍然保持连接。超过这个保活时间,如果检测不到对端,服务器就会断开连接,如果能够检测到对方,那么连接一直持续。
内核全局设置
内核的TCP/IP调优参数都位于/proc/sys/net/目录,可以直接写入数值或者采用sysctl命令或者系统调用。
1 2 3 4 |
#include <unistd.h> #include <linux/sysctl.h> int _sysctl(struct __sysctl_args *args); |
详细请参考:提高 Linux 上 socket 性能
常见的协议格式设计
记住,TCP是一种流协议
语出《Effective TCP/IP Programming》。意思是,TCP的数据是以字节流的方式由发送者传递给接收者,没有固有的“报文”或者“报文边界”的概念。简单说,TCP不理解应用层通信的协议,不知道应用层协议格式和边界。所以,所谓的“粘包和断包”是个伪概念。TCP压根就没有包边界的概念,何谈粘与断。
OSI模型定义的7层结构网络中,TCP协议所在的传输层和应用层之间还有会话层和表示层,原本协议包分界和加密等等操作是在这两层完成的。TCP/IP协议在设计的时候,并没有会话层和表示层。那如果用户需要这两层提供的服务怎么办?比如包的分界?答案是,用户自行在应用层代码中实现吧。
示例:
发送者发送三次
接收者可能收到这样:
避免分片的效率损失
数据链路层Maximum Transmission Unit(MTU, 最大传输单元)。
以太网通常在1500字节上下。所以单次发送的协议包数据最好小于这个值,从而避免IP层分片带来的效率损失。
扩展阅读:Linux TCP/IP协议栈关于IP分片重组的实现