TCP API

TCP 网络编程API

网络通信概要

网络进程如何通信?首要解决如何标识一个进程,本地可以用PID来解决,而网络中用IP+端口号来唯一标识一个进程。这样利用(IP、端口号、PID)可以唯一标识一个网络中的进程。

socket起源于UNINX,UNINX哲学之一就是一切皆文件,有一个打开、读写、关闭的模式来操作。socket就是该模式的一个实现,socket就是一种特殊的文件。

使用TCP/IP的协议应用程序采用应用编程接口 UNINX BSD关键字来实现网络进程通信,目前几乎所有的应用程序都是使用socket,网络通信无处不在。其基本模式包括:

1546948869237

  1. 服务器根据地址类型(IPV4、IPV6)创建socket
  2. 服务器为socket绑定IP地址和端口号
  3. 服务器socket监听端口号请求,随时准备接受客户端的连接,这时候服务器socket并未打开
  4. 客户端创建socket
  5. 客户端打开socket,根据服务器IP和端口号试图连接服务器socket
  6. 服务器socket接收到客户端socket请求,被动打开开始接受客户端请求,直到客户端返回连接信息。这时候socket进入阻塞状态
  7. 客户端连接成功,向服务器发送连接状态信息
  8. 服务器accept方法返回,连接成功。
  9. 服务器向socket写入信息
  10. 服务器读取信息
  11. 客户端关闭
  12. 服务器关闭

仔细研究会发现,服务器和客户端连接的部分就是三次握手。

网络编程API

1. socket函数

1
int socket(int domain, int type, int protocol)

socket 函数对应于普通文件的打开操作,普通文件返回一个描述字,而socket创建一个socket描述符,它唯一标识一个socket。

当调用socket时, 返回的描述他存在与协议族空间中,但是没有具体地址,如果想要赋予地址需要使用bind()函数。如果不绑定,系统会在使用时随机生成一个。

2. bind函数

bind() 函数把一个地址族中特定的地址赋给socket

1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

3. listen 和 connect 函数

这两个函数相对应,服务器调用listen函数来监听端口,而客户端调用connect函数去发出连接请求

1
2
int listen(int sockfd, int backlog)
int connect(int sockfd, const struct sockaddr *addr, docklen_t addrlen)

4. accept 函数

TCP 服务器依次调用socket()、bind()、listen()之后,就会监指定的socket地址了。而TCP客户端调用socket()、connect()之后就会向TCP服务器发送一个连接请求。TCP连接接收到这个请求之后,就会调用accept()函数接受请求,这样连接就建立了。

1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

注意:accept第一个参数为socket描述字,服务器调用socket生成的,称为监听socket关键字;而accept返回的是已连接socket关键字。一个服务器通常只创建一个监听socket描述字,这个描述子在服务器生命周期一直存在。内核为每个服务器接受的客户创建一个已连接socket关键字,当完成服务后就会关闭。

5. read 和 write 函数

read()函数从fd中读取内容,write写入内容。成功时返回字节数

6. close函数

1
int close(int fd);

TCP 协议选项

1. SO_REUSEADDR

一般来说一个端口释放后,大约两分钟才能再次使用(处于TIME_WAIT状态),而使用该选项可以使端口被释放后立即被使用(处于TIME_WAIT状态的端口)。

2. TCP_NODELAY/TCP_CHORK

网络拥塞领域,有一个非常著名的算法叫做Nagle算法, 这是以其发明人的名字命名的。John Nagle 首次用该算法解决福特公司的网络拥塞问题。该问题具体描述是:如果应用程序每次产生1Byte的数据,而这个1Byte的数据又以数据报的形式发送给网络服务器,那么很容易使得网络过载。所以传送1Byte的包却要花费40Byte的包头(IP20字节、TCP20字节)这种有效载荷利用低下的情况被称为愚蠢窗后症候群。

针对这问题,Nagle算法改进:如果发送少量字符包(小于MSS的包被称为小包,大于MSS的包被称为大包)发送端只会发送第一个小包,将后面的小包缓存起来。直到接收到前一个数据报的ACK为止,或当前字符较紧急,积攒了较多的数据。

TCP中Nagle算法默认启用,但是不使用任何情况。而TCP_NODELAY/TCP_CHORK字段控制了包的Nagle化。例如TCP_NODELAY便是直接把包发出去,这样用户体验会更好。

3. SO_LINGER

linger是延缓的意思,这里的延缓指close操作。默认close立即返回,但是当缓冲区还有部分数据时,系统会尝试将数据发送给对方。

4. TCP_DEAFER_ACCPET

实际上是接收到第一个数据包后,才会创建连接

5.SO_KEEPALIVE

SO_KEEPALIVE用来检测对方主机是否崩溃,避免服务器永远阻塞于TCP连接的输入

设置该选项后,如果2h内任何一方没有数据交换,TCP就会自动向对方发送一个保持存活探测,

  1. 对方接受正常,以ACK回应
  2. 对方已崩溃且重新启动,以RST响应,套接口错误置为restart,套接口本身被关闭
  3. 对方无响应,再次发送8个探测分节,11min15s 后无响应就放弃。

网络字节序与主机序

关于字节序再次讨论。不同的CPU有不同的字节序类型,这些字节序是整数在内存中的保存顺序,称为主机序。最常见的两种 1 小端, 低序的字节存在起始位置 2 大端 , 高字节的存在起始位置

小端法 地址低位存在值的低位,高位存值的高位。这种方式符合人的思维方式。

大端法 地址低位存高位的值。它很直观不需要考虑对应关系,只需要内存地址写出去即可

1547125473936

为什么要注意字节问题呢?C++编译平台的储存是由编译平台的CPU确定的,而JAVA编写的程序唯一采用大端法。所有网络协议都是大端法。其也被称为网络字节序。

封包和解包

TCP是一个流协议,所谓流就行没有界限的一串数据。但是通信程序是需要独立的数据包发送。

设想这样几种情况

  1. 先接收到data1 后接到 data2
  2. 先接收到data1 的部分数据,后接到data1的余下部分与 data2全部
  3. 先接收到了data1全部数据和部分data2数据,后接受到data余下数据
  4. 一次接受data1和data2全部数据

1是理想情况 而2、3、4即使粘包的情况,这时就需要拆包将受到的数据拆成独立的数据包。这种情况可能有这么几种原因。

1 由Nagle算法造成的粘包

2 接收端不及时接受造成的粘包

具体的解决办法便是封包和拆包。封包是给一段数据加上包头,这样数据就分为包头、包体两部分。包头包含了一个结构体成员变量表示了包体的长度,这样另一方便可以通过这个长度进行拆包;