网络通信(TCP)
TCP(面向连接的通信协议):在通信中时刻保持连接,这种通信方式类似于打电话,能保证安全可靠数据不丢失,但是与UDP相比传输速度较低。
TCP和UDP通信收发函数区别:记住socket中是否存放有IP 和端口信息,TCP有,UDP无。同时TCP服务器需要多个 socket 对象,对应多个连接。
TCP编程模型:
进程A:创建socket->准备地址->绑定->监听(设置队列长度)->等待连接->通信->关闭socket
进程B:创建socket->准备地址->连接->通信->关闭socket
listen函数:设置socket最大的排队数量(监听)
int listen(int sockfd, int backlog);
accept函数:等待其他主机与当前socket建立连接关系
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- accept()的返回值:成功返回建立连接成功的描述符,此后的通信都用此描述符,失败返回-1
recv函数:TCP网络通信专用的数据接收
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- sockfd:此时为accept的返回值
- buf:数据缓冲区
- len:缓冲区大小
- flags:一般为0
send函数:TCP网络通信专用的数据发送
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
实现代码 编写一个服务端程序(子进程来处理客户端发来的消息)。
什么是TCP的三次握手?
SYN:同步序列编号(Synchronize Sequence Numbers)。是TCP/IP建立连接时使用的握手信号。
ACK:ACK (Acknowledgement)即是确认字符,在数据通信中,接收站发给发送站的一种传输类控制字符。表示发来的数据已确认接收无误。在TCP/IP协议中,如果接收方成功的接收到数据,那么会回复一个ACK数据。通常ACK信号有自己固定的格式,长度大小,由接收方回复给发送方。
FIN:FIN(finish)为TCP报头的码位字段,该位置为1的含义为发送方字节流结束,用于关闭连接。当两端交换带有FIN标志的TCP报文段并且每一端都确认另一端发送的FIN包时,TCP连接将会关闭。FIN位字面上的意思是连接一方再也没有更多新的数据发送。然而,那些重传的数据会被传送,直到接收端确认所有的信息。
- 第一次握手:客户端发送syn包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认;
- 第二次握手:服务器收到syn包,必须确认客户的syn(ack=j+1),同时自己也发送一个SYN包(syn=k),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。连接建立后,客户端和服务器就可以开始进行数据传输了。
为什么需要三次握手?
- 考虑一次的问题,首先TCP是面向连接,一次握手肯定建立不了连接,正如将石头扔入井中,只有听到响声才会知道落水了,因此客户端给服务器发出请求信息却没有得到回应,客户端是没法判断是否发送成功然后建立连接的。
- 如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。
- 如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。三次握手就是为了防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
什么是四次挥手?
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
- 1、 TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。
- 2、 服务器收到这个FIN,它会先发回一个ACK,因为如果此时服务端到客户端还有数据未传输完,则还需要传输数据,此时收到ACK确认的客户端会进入FIN_WAIT状态。
- 3、 当服务器数据传输完成,觉得可以关闭与客户端的连接时,会发送一个FIN给客户端。
- 4、 客户端收到服务端的FIN后,知道可以真正关闭连接了,则会发回ACK报文确认,但怕由于网络原因服务端接收不到ACK,客户端会在发送后进入TIME_WAIT,以防止服务端没有收到ACK可以进行重传。当客户端等待了2MSL后没有收到回应,则证明服务端收到ACK并关闭了,那么客户端也就自己关闭了连接。
提问1:TCP是面向连接的,三次握手后,源IP一定是真实的?
答案:错误。
TCP SYN 泛洪:
对于TCP协议,当客户端向服务器发起连接请求并初始化时,服务器一端的协议栈会留一块缓冲区来处理“握手”过程中的信息交换。请求建立连接时发送的数据包的包头SYN位就表明了数据包的顺序,攻击者可以利用在短时间内快速发起大量连接请求,以致服务器来不及响应。同时攻击者还可以伪造源IP地址。也就是说攻击者发起大量连接请求,然后挂起在半连接状态,以此来占用大量服务器资源直到拒绝服务。虽然缓冲区中的数据在一段时间内(通常是三分钟)都没有回应的话,就会被丢弃,但在这段时间内,大量半连接足以耗尽服务器资源。
TCP LAND:
LAND攻击利用了TCP连接建立的三次握手过程,通过向一个目标主机发送一个用于建立请求连接的TCP SYN报文而实现对目标主机的攻击。与正常的TCP SYN报文不同的是:LAND攻击报文的源IP地址和目的IP地址是相同的,都是目标主机的IP地址。这样目标主机接在收到这个SYN 报文后,就会向该报文的源地址发送一个ACK报文,并建立一个TCP连接控制结构,而该报文的源地址就是自己。由于目的IP地址和源IP地址是相同的,都是目标主机的IP地址,因此这个ACK 报文就发给了目标主机本身。
提问2:为什么TIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态?
答:首先说明一下什么是2MSL?MSL即Maximum Segment Lifetime,也就是报文最大生存时间,引用《TCP/IP详解》中的话:“它(MSL)是任何报文段被丢弃前在网络内的最长时间。”那么,2MSL也就是这个时间的2倍。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
什么是可靠的连接(A要知道:A->B&&B->A ,B要知道:B->A&&A->B);
服务端server:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
typedef struct sockaddr* saddrp;
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));//通过命令行设置端口与ip
addr.sin_addr.s_addr = inet_addr(argv[2]);
int ret = bind(sockfd,(saddrp)&addr,sizeof(addr));
if (0 > ret)
{
perror("bind");
return -1;
}
listen(sockfd,1024);//设置监听数量
struct sockaddr_in client_addr = {};
socklen_t addr_len = sizeof(client_addr);
bool quit_flag = false;//进程结束标志
while(1)
{
int client_fd = accept(sockfd,(saddrp)&client_addr,&addr_len);
if(0 == fork())
{
while(1)
{
char buf[255] = {};
int val = recv(client_fd,buf,sizeof(buf),0);
if(0 == strcmp(buf,"q"))
{
quit_flag = true;
break;
}
sprintf(buf,"Server ID:%d:%s",getpid(),"I will reply u soon");
send(client_fd,buf,strlen(buf)+1,0);
}
close(client_fd);
if(quit_flag == true) exit(0);
}
}
close(sockfd);
return 0;
}
客户端client:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
typedef struct sockaddr* saddrp;
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if (0 > sockfd)
{
perror("socket");
return -1;
}
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[1]));
addr.sin_addr.s_addr = inet_addr(argv[2]);
int ret =connect(sockfd,(saddrp)&addr,sizeof(addr));
if (0 > ret)
{
perror("connect");
return -1;
}
while(1)
{
char buf[255] = {};
printf(">");
gets(buf);
send(sockfd,buf,strlen(buf)+1,0);
if(0 == strcmp(buf,"q")) break;
ret = recv(sockfd,buf,sizeof(buf),0);
if (0 > ret)
{
perror("read");
return -1;
}
printf("Recv:%d Bytes.\nShow:%s\n",ret,buf);
if(0 == strcmp(buf,"q")) break;
}
close(sockfd);
return 0;
}
运行结果:
返回