1.TCP四次挥手
①断开时,当A说不玩了,就进图FIN_WAIT_1状态,B收到A不玩了的消息,发送知道了,就进入CLOSE_WAIT状态。
A收到B说知道了,就进入FIN_WAIT_2状态,如果这时候B直接跑路,A将永远在这个状态。
TCP协议里面没有对这个状态处理,Linux有:可调整tcp_fin_timeout这个参数,设置一个超时时间。
②如果B没有跑路,发送B也不玩了的请求到达A时,A发送知道B也不玩了的ACK后,从FIN_WAIT_2状态结束,万一B收不到ACK?
B会重新发一个B不玩了,此时A最后等待一段时间TIME_WAIT,要足够长,长到如果B没收到ACK的话,B说不玩了会重发,A会重新发一个ACK并且足够时间到达B。
③A直接跑路,A的端口直接空出来,但B不知道,B原来发过很多包很可能在路上,如果A的端口被一个新的应用占用,这个新的应用会收到上个连接中B发过来的包,虽然序列是重新生成的,但这里要上一个双保险,防止产生混乱,也需要等足够长时间,等到原来B发送的所有包都死掉,再空出端口来。
④等待时间设为2MSL, MSL(Maximum Segment Lifetime),报文最大生存时间,是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
因为TCP报文基于IP协议,IP头上有一个TTL域,是IP数据包课经过的最大路由数,每经过一个处理它的路由器,此值就减1,当此值为0则数据包被丢弃,同时发送ICMP报文通知源主机。
协议规定MSL为2分钟,实际应用中常用的是30s,1min和2min等。
2.如何实现一个靠谱的协议?
①客户端发送一个包,服务器端都应该有个回复,如果服务器端超过一定时间没有回复,客户端就会重新发送这个包,直到有回复。
②上一个收到应答,再发送下一个。缺点是效率低。
③TCP协议为了保证顺序性,每一个包都有一个ID。在建立连接时,会商定起始的ID是什么,然后按照ID一个个发送。
为了保证不丢包,对于发送方的包都要进行应答, 但这个应答不是一个一个来的,而是会应答某个之前的ID,表示都收到了,称为累计确认或累计应答。
3.顺序问题与丢包问题
①假设4的确到了,5的ACK丢了,6,7的数据包丢了,该怎么办?
超时重试:对每一个发送了但没有ACK的包,都有设一个定时器,超过一定时间,就重新尝试。
②超时时间如何评估?
不宜过短,时间必须大于往返时间RTT,否则会引起不必要的重传。不宜过长,访问就变慢了。
自适应重传算法:需要TCP通过采样RTT时间,进行加权平均,算出一个值,而且这个值要不断变化,因为网络状况不断变化。还要采样RTT波动范围,计算出一个估计的超时时间。
③快速重传机制
当接收方收到一个序号大于下一个期望的报文段时,就会检测到数据流中的一个间隔,就会发送冗余的ACK,仍然ACK的是期望接收的报文段。
当客户端收到3个冗余的ACK后,就会在定时器过期之前,重传丢失的报文段。
4.流量控制问题
当接收方比较慢时,要防止低能窗口综合征:
只要空出一个字节来就赶快告诉发送方,然后马上又填满了,当窗口太小时,不更新窗口,直到达到一定大小或者缓冲区一半为空,才更新窗口。
5.拥塞控制问题
通过拥塞窗口cwnd的大小来控制,怕把网络塞满。
对于网络上,通道的容量=带宽×往返延迟。
如果我们设置发送窗口,使得发送但未确认的包为通道容量,就就能撑满整个管道。
TCP拥塞控制主要来避免两种现象:丢包和超时重传。
一旦出现说明发送速度太快了,要慢一点。但一开始不知道有多快,如何知道窗口调整到多大?
①慢启动
如同漏斗灌水,一开始慢慢的倒,发现总能倒进去,就越倒越快。
②拥塞控制
有一个阈值,当超过这个值时,就要小心点,不能倒这么快了,可能快满了,再慢下来。
③快速恢复
当接收端发现丢了一个中间包时,发送三次前一个包的ACK,发送端就会快速重传,不必等待超时再重传。TCP认为这种情况不严重,因为大部分没丢,只丢了一小部分。
6.TCP BBR拥塞算法
①公网上带宽不满也会丢包,并不能认为就是拥塞了。
②TCP拥塞控制要等到中间设备填满才发生丢包,再降低速度就晚了。
正确做法:
TCP只要填满管道就可以,不应该接着填,直到连缓存也填满。
7.套接字编程
①低地址高字节:
TCP/IP协议规定,网络数据流应采用大端字节序。
为了使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,调用以下库函数做网络字节序和主机字节序的转换。
h:host
n:network
I:32位长整数
s:16位短整数
#include <stdio.h>
int main(void) {
int a = 321;
char *p;
p = (char *)&a;
printf("addr=%p, val=%#x\n", p, *p);
printf("addr=%p, val=%#x\n", p+1, *(p+1));
printf("addr=%p, val=%#x\n", p+2, p[2]);
printf("addr=%p, val=%#x\n", p+3, p[3]);
printf("%#x\n", 321);
return 0;
}
#include <stdio.h>
#include <arpa/inet.h>
int main(void) {
int a = 0x12345678;
printf("%#x\n", a);
int b = htonl(a);
printf("%#x\n", b);
return 0;
}
8.socket地址的数据类型
socket API:一层抽象的网络编程接口,适用于各种底层网络协议,
如:IPv4,IPv6,及Unix Domain Socket。各种网络协议的地址格式不相同。
IPv4,IPv6,及Unix Domain Socket的地址类型分别定义为常数AF_INET,AF_INET6,AF_UNIX。
只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
9.基于TCP协议的网络程序
建立连接后,TCP协议提供全双工的通信服务,但是一般客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。
服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器应答,服务器调用write()将处理结果发送给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
如果客户端没有更多请求,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就 知道客户端关闭了连接,也调用close()关闭连接。
注意:任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍然可接收对方发来的数据。
server:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#define SERV_PORT 8000
#define MAXLINE 80
int main(void) {
struct sockaddr_in serveraddr, clientaddr;
int listenfd, connfd;
socklen_t clientaddr_len;
char buf[MAXLINE];
char str[INET_ADDRSTRLEN];
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(SERV_PORT);
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, 3);
printf("Accepting connections...\n");
while (1) {
clientaddr_len = sizeof(clientaddr);
connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddr_len);
printf("Received from %s:%d\n", inet_ntop(AF_INET, &clientaddr.sin_addr, str, sizeof(str)), ntohs(clientaddr.sin_port));
int n = read(connfd, buf, MAXLINE);
for (int i = 0; i < n; i++) {
buf[i] = toupper(buf[i]);
}
write(connfd, buf, n);
close(connfd);
}
return 0;
}
client:
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <strings.h>
#include <unistd.h>
#include <ctype.h>
#include <arpa/inet.h>
#include <string.h>
#define SERV_PORT 8000
#define MAXLINE 80
int main(void) {
struct sockaddr_in servaddr;
char buf[MAXLINE] = {"hello tcp"};
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
write(sockfd, buf, strlen(buf));
int n = read(sockfd, buf, MAXLINE);
printf("Response from server:\n");
write(1, buf, n);
close(sockfd);
return 0;
}