5~9章

五.TCP原理

5.1 TCP Socket中的IO缓冲

image-20221129174333338

TCP Socket的数据无边界,即writeread次数并不对应,多次发送的数据,可以通过read一次完成读取,一次发送的数据,也可以每次接收一部分,多次完成读取。

这主要是通过IO缓冲完成的。

调用write函数时,数据并未发送,而是移到输出缓冲,在适当的时候传向对方的输入缓冲

另一方可调用read函数从输入缓冲读取数据。这些IO缓冲特性如下:

  • IO缓冲在每个TCP Socket中单独存在
  • IO缓冲在创建Socket时自动生成
  • 即使关闭Socket也会继续发送输出缓冲中遗留的数据
  • 关闭Socket将丢失输入缓冲中的数据

如果一次发送很大的数据,对方的输入缓冲放不下怎么办?

TCP使用滑动窗口,如果放不下,就停止发送了。

write函数返回的时间点

write函数并不是在完成向对方主机的数据传输后才返回,而是在数据移到输出缓冲时。剩下的事情由TCP完成,TCP会完成对输出缓冲数据的传输,

因此write函数是在数据传输到输出缓冲时返回。

5.2 TCP连接

image-20221129175553595

SEQ和ACK都以字节为单位,ACK指的是想要接收的下一个数据包的首字节编号

所以如果SEQ = 1200,发送100字节数据,那么下一个ACK就是1301

5.3 发送数据

image-20221129175818774

5.4 断开连接

image-20221129175857184

A通知B要断开连接

B通知A:知道了,请稍等,我做好扫尾工作

B准备好后通知A:可以关闭连接了

A通知B:好的

为什么ACK两次都是5001?

因为B第一次发送的ACK = 5001没有收到数据,所以数据编号不变

六、基于UDP的C/S

  1. UDP无需建立连接,就可以发送
  2. UDP的server和client端都只需一个Socket,而TCP中,服务端需要为每个客户端创建一个Socket
  3. 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个阶段

  1. 在UDP Socket中注册目标IP和Port
  2. 传输数据
  3. 删除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

之后,就可以使用sendtorecvfrom进行收发数据,也可使用readwriet通信

write(sock, msg, strlen(msg));
read(sock, msg, sizeof(msg) - 1);
printf("Receive From Server: %s\n", msg);
close(sock);

七、半断开

  1. socket由两个流组成: 输入流输出流
  2. 可以精细化操作,单独关闭输入或输出流

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 关闭流的总结

  1. 调用close()

    close(socket)时,会向对方发送一个RST报文,解释如下

    RST(Reset)标志位用于中断(reset)TCP连接。当TCP报文段中的RST标志位被设置为1时,它表示发送方或接收方希望立即中止连接,并且不希望继续进行TCP通信。
    
  2. 半关闭

    1. 关闭输入流时,不会发送任何报文,但如果之后收到对方发来的数据,将不会接收,同时回复RST报文

      关闭输入流之后,仍然可以读取输入缓冲中的数据,因此可以读取关闭之前接收到的数据。

    2. 关闭输出流时,会发送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

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 image-20230715190641508
  • 协议是分层的,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

在运行程序时候,有时候会出现bind() error错误,这是因为端口处在TIME_WAIT状态,因此无法被使用,一般等待几分钟即可

  1. 在TCP的四次挥手过程中,TIME_WAIT状态由主动发起关闭的一方承担。
  2. 之所以有这个状态,是担心最后一个报文丢失,接收方重传,此时发起方已经关闭,导致无法回复的情况。

可以通过设置SO_REUSEADDR1,避免这种情况

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算法

posted @ 2024-07-03 00:11  INnoVation-V2  阅读(2)  评论(0编辑  收藏  举报