5~9章
五.TCP原理
5.1 TCP Socket中的IO缓冲
![image-20221129174333338](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221129174333338.png)
TCP Socket的数据无边界,即write
和read
次数并不对应,多次发送的数据,可以通过read一次完成读取,一次发送的数据,也可以每次接收一部分,多次完成读取。
这主要是通过IO缓冲完成的。
调用write函数时,数据并未发送,而是移到输出缓冲
,在适当的时候传向对方的输入缓冲
。
另一方可调用read函数从输入缓冲
读取数据。这些IO缓冲特性如下:
- IO缓冲在每个TCP Socket中单独存在
- IO缓冲在创建Socket时自动生成
- 即使关闭Socket也会继续发送输出缓冲中遗留的数据
- 关闭Socket将丢失输入缓冲中的数据
如果一次发送很大的数据,对方的输入缓冲放不下怎么办?
TCP使用滑动窗口,如果放不下,就停止发送了。
write函数返回的时间点
write函数并不是在完成向对方主机的数据传输后才返回,而是在数据移到输出缓冲时。剩下的事情由TCP完成,TCP会完成对输出缓冲数据的传输,
因此write函数是在数据传输到输出缓冲时返回。
5.2 TCP连接
![image-20221129175553595](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221129175553595.png)
SEQ和ACK都以字节为单位,ACK指的是想要接收的下一个数据包的首字节编号
所以如果SEQ = 1200,发送100字节数据,那么下一个ACK就是1301
5.3 发送数据
![image-20221129175818774](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221129175818774.png)
5.4 断开连接
![image-20221129175857184](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221129175857184.png)
A通知B要断开连接
B通知A:知道了,请稍等,我做好扫尾工作
B准备好后通知A:可以关闭连接了
A通知B:好的
为什么ACK两次都是5001?
因为B第一次发送的ACK = 5001没有收到数据,所以数据编号不变
六、基于UDP的C/S
- UDP无需建立连接,就可以发送
- UDP的server和client端都只需一个Socket,而TCP中,服务端需要为每个客户端创建一个Socket
- UDP的send和recv的次数要一一对应
6.1 UDP相关的函数
#include <sys/socket.h>
/*
sock: 用于传输数据的UDP Socket
buf: 待传输数据所在地址
nbytes: 待传输数据长度,单位为B
flags: 可选项参数
to: 目标地址
addrlen: to结构体的长度
*/
//成功返回发送字节数,失败-1
ssize_t sendto(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
/*
sock: 用于传输数据的UDP Socket
buf: 待传输数据所在地址
nbytes: 待传输数据长度,单位为B
flags: 可选项参数
from: 存有发送方地址信息的结构体
addrlen: from结构体的长度
*/
//成功返回接收字节数,失败-1
ssize_t recvfrom(int sock, void *buf, size_t nbytes, int flags, struct sockaddr *from, socklen_t addrlen )
6.2 对UDP Socket进行Connect
在UDP中,通过sendto
进行数据传输的过程大致分为3个阶段
- 在UDP Socket中注册目标IP和Port
- 传输数据
- 删除UDP Socket中注册的地址信息
因此只需一个Socket,UDP就能向多个Socket发送数据,这种Socket被称作未连接Socket。
但是当遇到如下情况时,情况显得不太合理
向同一目标发送5个数据包,且数据包不能合并
此时就需要重复上述阶段,第1、3步需要重复执行5次,完全是浪费资源和时间,这种情况下,可以对Socket进行Connect,Connect的UDP Socket只需要注册一次目标信息,之后就可以不停地发送。
sock = socket(PF_INET, SOCK_DGRAM, 0);
struct 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(argv[1]);
server_addr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr));
和TCP创建过程一致,但是创建Socket的第二个参数是SOCK_DGRAM
,说明创建的是UDP
之后,就可以使用sendto
和recvfrom
进行收发数据,也可使用read
和wriet
通信
write(sock, msg, strlen(msg));
read(sock, msg, sizeof(msg) - 1);
printf("Receive From Server: %s\n", msg);
close(sock);
七、半断开
- socket由两个流组成:
输入流
和输出流
- 可以精细化操作,单独关闭输入或输出流
7.1 半关闭
#include<sys/socket.h>
/*
成功返回0, 失败-1
sock: 要断开的套接字
howto: 要断开哪条流
SHUT_RD: 断开输入流
SHUT_WR: 断开输出流
SHUT_RDWR:断开IO流
*/
int shutdown(int sock, int howto);
7.2 关闭流的总结
-
调用
close()
close(socket)
时,会向对方发送一个RST报文
,解释如下RST(Reset)标志位用于中断(reset)TCP连接。当TCP报文段中的RST标志位被设置为1时,它表示发送方或接收方希望立即中止连接,并且不希望继续进行TCP通信。
-
半关闭
-
关闭输入流时,不会发送任何报文,但如果之后收到对方发来的数据,将不会接收,同时回复
RST报文
。关闭输入流之后,仍然可以读取
输入缓冲
中的数据,因此可以读取关闭之前接收到的数据。 -
关闭输出流时,会发送
FIN报文
-
八、DNS
因为IP经常变换,而域名相对稳定,因此使用DNS动态获取IP地址
8.1 利用域名获取IP地址
#include<netdb.h>
struct hostent {
char *h_name; // 官方域名
char **h_aliases; // alias list, 多个别名指向同一个official name
int h_addrtype; // 支持的通信类型IPV4或IPV6
int h_length; // IP地址长度
char **h_addr_list;// IP地址列表,一个域名可能有多个IP地址
};
struct hostent* gethostbyname(const char* hostname);
![image-20221130161008112](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20221130161008112.png)
8.2 利用IP地址获取域名
#include<netdb.h>
/*
失败返回NULL指针
1.addr: 包含地址信息的in_addr结构体指针。为了同时传递IPv4地址之外的其他信息,该变量的类型声明为char指针
2.len: 向第一个参数传递的地址信息的字节数,1Pv4时为4,IPv6时为16。
3.family: 传递地址族信息,1PV4时为AF_INET,IPv6时为AF_INET6。
*/
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);
九.socket修改参数
9.1 常见Socket选项
![image-20230715190629741](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20230715190629741.png)
![image-20230715190641508](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20230715190641508.png)
- 协议是分层的,
IPPROTO_IP
是IP层相关选项,IPPROTO_TCP
是TCP相关选项,SOL_SOCKET
是socket的通用选项 - 大多数选项既可以读,也可以修改,但是有的选项是只读的
9.2 查看、修改Socket参数
getsockopt()
#include<sys/socket.h>
/*
成功0,失败-1
1.sock: socket
2.level: 要查看的选项所在协议层
3.optname: 要查看的选项名
4.optval: 保存结果的地址值
5.optlen: optval的大小。保存第四个参数返回的选项信息的字节数
*/
int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);
setsockopt()
#include<sys/socket.h>
/*
成功0,失败-1
1.sock: socket
2.level: 要修改的选项所在协议层
3.optname: 要修改的选项名
4.optval: 保存选项信息的地址值
5.optlen: 第四个参数的字节数
*/
int setsockopt(int sock, int level, int optname, const void* optval, socklen_t* optlen);
9.3 修改TCP缓冲大小
1.SO_SNDBUF: 输入缓冲大小
2.SO_RCVBUF: 输出缓冲大小
// 1.读取输入缓冲大小
sock = socket(PF_INET, SOCK_STREAM, 0);
int snd_buf;
socklen_t len = sizeof(snd_buf);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
// 2.修改输出缓冲大小
sock = socket(PF_INET, SOCK_STREAM, 0);
int snd_buf = 1024*3;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
9.4 SO_REUSEADDR
![image-20230715192413214](http://pic-save-fury.oss-cn-shanghai.aliyuncs.com/uPic/image-20230715192413214.png)
在运行程序时候,有时候会出现bind() error
错误,这是因为端口处在TIME_WAIT
状态,因此无法被使用,一般等待几分钟即可
- 在TCP的四次挥手过程中,
TIME_WAIT
状态由主动发起关闭的一方承担。 - 之所以有这个状态,是担心最后一个报文丢失,接收方重传,此时发起方已经关闭,导致无法回复的情况。
可以通过设置SO_REUSEADDR
为1
,避免这种情况
int option = 1;
setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, sizeof(option));
9.5 TCP_NODELAY
首先简单介绍naggle算法,Nagle算法是一个流量控制算法,用于优化TCP连接中的数据传输效率。它通过延迟发送小数据包,将多个小数据包合并成一个大的数据包进行发送,以减少网络中的小数据包数量,提高传输效率。
naggle算法简单流程如下
1.当调用write发送数据时,数据被存储在发送缓冲中,不会立即发送。
2.当发送缓冲区达到一定大小(通常是最大报文段长度MSS)时,触发发送
3.或者200ms内没有新的数据写入时,也会发送,如果在等待时间内有新数据写入,则会重置等待
4.在等待时收到的之前消息的ACK,则会将多个ACK合并为一个ACK到待发送的数据包中,以减少ACK的数量
总而言之,Nagle算法可以提高网络传输效率,但可能会导致一定的延迟,因为他不会立刻发送数据,因此可以通过设置TCP_NODELAY = 1
关掉naggle算法