注:TCP和UDP可以同时使用相同的端口号,同时运行,因为系统可以分辨出是流式服务还是数据报,以给不同的协议。
同一个进程可以创建多个套接字。
TCP 编程流程
基础概念
TCP 提供的是面向连接的、可靠的、字节流式服务。TCP 的服务器端和客户端编程流程如 下:
目的:实现服务器端与客户端的数据通信。
连接性:三次握手,四次挥手
可靠性:应答确认,超时重传机制 ; 乱序重排:依靠序列号,去重,排序;滑动窗口,流量控制。
流式服务:发送次数与接收次数没有关系;会造成粘包问题
TCP服务端 | TCP客户端 |
---|---|
socket() | socket |
bind() | |
listen() | connect |
accept() | |
recv() | send() |
send() | recv() |
close | close() |
接口与链接
接口
socket()
方法是用来创建一个套接字,有了套接字就可以通过网络进行数据的收发。这也 是为什么进行网络通信的程序首先要创建一个套接字。创建套接字时要指定使用的服务类 型,使用 TCP 协议选择流式服务(SOCK_STREAM)。
bind()
方法是用来指定套接字使用的 IP 地址和端口。IP 地址就是自己主机的地址,如果 主机没有接入网络,测试程序时可以使用回环地址“127.0.0.1”。端口是一个 16 位的整形值, 一般 0-1024 为知名端口,如 HTTP 使用的 80 号端口。这类端口一般用户不能随便使用。其 次,1024-4096 为保留端口,用户一般也不使用。4096 以上为临时端口,用户可以使用。在 Linux 上,1024 以内的端口号,只有 root 用户可以使用。
listen()
方法是用来创建监听队列。监听队列有两种,一个是存放未完成三次握手的连接, 一种是存放已完成三次握手的连接。listen()第二个参数就是指定已完成三次握手队列的长度。 accept()处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
accept()
仅处理存放在 listen 创建的已完成三次握手的队列中的连接。每处理一个连接,则 accept()返回该连接对应的套接字描述符。如果该队列为空,则 accept 阻塞。
connect()
方法一般由客户端程序执行,需要指定连接的服务器端的 IP 地址和端口。该方法执行后,会进行三次握手, 建立连接
send()
方法用来向 TCP 连接的对端发送数据。send()执行成功,只能说明将数据成功写入 到发送端的发送缓冲区中,并不能说明数据已经发送到了对端。send()的返回值为实际写入 到发送缓冲区中的数据长度。
recv()
方法用来接收 TCP 连接的对端发送来的数据。recv()从本端的接收缓冲区中读取数 据,如果接收缓冲区中没有数据,则 recv()方法会阻塞。返回值是实际读到的字节数,如果 recv()返回值为 0, 说明对方已经关闭了 TCP 连接。
close()
方法用来关闭 TCP 连接。此时,会进行四次挥手。
TCB协议 报头
连接报文:SYN
断开报文:FIN
确认报文:ACK
错误报文:RST
三次握手
链接的开始:connect(),通过三次握手实现,报文 SYN ;结束:connect() 返回成功时,三次握手结束。
第一次握手:客户端给服务端发送报文 SYN 以及32位序号;比如:i,
第二次握手:服务器端收到后返回自己的32位序号(SYN ;序号J),以及确认号(确认报文:ACK;确认序号:客户端的序号+1):32位序号:SYN J,确认信息:ACK i+1;
第三次握手:客户端收到后,返回给服务器端确认信息:(ACK J+1);
服务器端 | 报头传递 | 客户端 | |
---|---|---|---|
第一次握手: | 收 | SYN i | 发 |
第二次握手: | 发 | SYN j;ACK i+1 | 收 |
第三次握手: | 收 | ACK j+1 | 发 |
收发数据 : | recv | send | |
send | recv |
收发数据:连接成功后才能收发数据
listen():创建两个监听序列如下:
未完成三次握手序列:放着未完成三次握手的客户端
已完成三次握手序列:当上面序列中的客户端 三次握手完成后,移入该序列。
注:如果客户端不停的只发送SYN,会造成服务器端listen监听队列拥挤,恶意攻击服务器端。
四次挥手
关闭TCB连接:close()或直接结束进程,通过四次挥手实现:结束报文FIN
第一次挥手:可以由服务器端或者客户端任意一个开始挥手(先执行到close的一段),通知准备结束链接:将结束报文(FIN)和自己的序号(假如m)发送过去。
第二次挥手:另一端接收到信息后返回确认报文(ACK)以及确认序号(m+1);
第三次挥手:当另一端也执行到close了,开始第三次挥手,将自己的序列号发送给另一端(FIN n)
第四次挥手:另一端接收到信息后回复确认报文及确认序号(ACK n+1);
注:
存在特殊情况只进行了三次挥手。
如果第四次挥手中,该端忽略ACK,恶意不停的发送FIN,会导致另一端的timewait不断的重新计时,该端会被持续占用。
服务器端 | 报头传递 | 客户端 | |
---|---|---|---|
第一次挥手: | 收 | FIN m | 发(假设先close) |
第二次挥手: | 发 | ACK m+1 | 收 |
第三次挥手: | 发(close) | FIN n | 收 |
第四次挥手 | 收 | ACK n+1 | 发 |
观察握手挥手(抓包)
管理员模式下使用tcpdump命令。
tcp 粘包问题(流式服务特点)
发送端连续多次send()写入发送缓冲区,然后发送到接收端的接收缓冲区,reccv()从接收缓冲区读数据时,仅根据设置的读取长度读,因此存在将数据读在一起的情况。
例:发送端:abc def ghi
接收端:abcdefghi
解决办法:
前后加标记
将send分开,send一次recv一次
timewait状态
timewait状态:指某一端将依旧保持存在的状态两分钟左右。
服务器端或者客户端谁先关闭,谁最后停留在timewait状态,二者同时关闭时,同时发送fin,最后都进入timewait状态。
意义:
可靠的终止 TCP连接。保证双方完全关闭
保证让迟来的TCP 报文段有足够的时间被识别并丢弃。将网络中残余的数据处理干净,防止影响下一次连接。
模拟实现TCP
服务器端:
lcx@lcx-virtual-machine:~/mycode/1.8$ vi ser.c 1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 int main() 11 { 12 int sockfd =socket(AF_INET,SOCK_STREAM,0);//创建套接字,他能通过网络进行数据的收发 13 assert(sockfd!=-1); 14 15 struct sockaddr_in saddr,caddr;//定义端口地址结构体 16 memsetu(&saddr,0,sizeof(saddr)); 17 saddr.sin_family=AF_INET;//IPV4 18 saddr.sin_port=htons(6000);//端口:转成网络端口(大端) 19 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//地址(字符串转整形) 20 //指定套接字的ip,port 绑定 21 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); 22 assert(res!=-1); 23 //设置监听队列的大小:5 24 res=listen(sockfd,5); 25 assert(res!=-1); 26 while(1) 27 { 28 int len=sizeof(caddr); 29 int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//接受链接,如果监听队列为空阻塞住,仅能处理已完成三次握手的监听队列 30 //c是链接套接字,sockfd 是监听套接字 31 if(c<0) 32 { 33 continue;//不成功重新接收 34 } 35 printf("accept c=%d\n,caddr.ip=%s,caddr.port=%d\n",c,inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));//打印客户端的详细信息,链接套接字,客户端ip,客户端端口 35 while(1)//循环接收数据 36 { 36 char buff[128]={0}; 37 int num=recv(c,buff,127,0);//接收数据,返回数据长度 37 if(n==0)break; //判断:当客户端close后接收数据长度为0,退出循环 38 printf("recv(%d)=%s\n",num,buff);//打印 39 send(c,"ok",2,0);//返回ok 40 } 40 printf("client close\n"); 41 close(c);//关闭链接套接字 43 } 44 close(sockfd); 45 exit(0); 46 47 48 } 192.168.74.128 lcx@lcx-virtual-machine:~/mycode/1.8$ gcc -o ser ser.c lcx@lcx-virtual-machine:~/mycode/1.8$ ./ser accept c=4,caddr.ip=127.0.0.1,caddr.port=60418 recv:(6)=hello recv:(4)=abc recv:(5)=1234 cilent close accept c=4,caddr.ip=127.0.0.1,caddr.port=60420 recv:(4)=abc recv:(5)=1234 cilent close
客户端
lcx@lcx-virtual-machine:~/mycode/1.8$ vi cli.c 1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 int main() 11 { 12 int sockfd=socket(AF_INET,SOCK_STREAM,0);//创建套接字 13 assert(sockfd!=-1); 14 15 struct sockaddr_in saddr;//定义服务器地址结构体 16 memset(&saddr,0,sizeof(saddr)); 17 saddr.sin_family=AF_INET; 18 saddr.sin_port=htons(6000); 19 saddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 20 21 int res=connect(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//申请链接,进行三次握手 22 assert(res!=-1); 23 24 printf("intput:\n"); while(1)//循环发送数据 { 25 char buff[128]={0}; 26 fgets(buff,127,stdin);//键盘获取数据 27 if(strncmp(buff,"end"),3==0) break;//输入end结束循环 28 send(sockfd,buff,strlen(buff),0);//发送数据 29 memset(buff,0,128);//置空 30 recv(sockfd,buff,127,0);//接收数据 31 printf("buff=%s\n",buff); 32 } 33 close(sockfd); 34 exit(0); 35 36 } ~ ~ lcx@lcx-virtual-machine:~/mycode/1.8$ gcc -o cli cli.c lcx@lcx-virtual-machine:~/mycode/1.8$./cli input: hello buff=ok input: abc buff=ok intput: 1234 buff=ok intput end lcx@lcx-virtual-machine:~/mycode/1.8$./cli input: abc buff=ok intput: 1234 buff=ok intput: end
多线程实现TCP
为了解决可以同时接收多个客户端的数据的问题,我们使用多线程处理
优点:开销小,线程共享主线程资源
缺点:一个线程崩掉会导致整个进程强制结束
1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 9 9 void* recv_thread(void* arg) 9 { 10 int c=(int)arg; 11 while(1) 11 { 14 char buff[128]={0}; 13 int n=recv(c,buff,127,0); 12 if(n<=0) 15 break; 14 printf("recv(%d)=%s\n",c,buff); 16 send(c,"ok",2,0); 17 } close(c); 11 printf("client close\n"); 13 } 11 10 int main() 11 { 12 int sockfd =socket(AF_INET,SOCK_STREAM,0);//创建套接字,他能通过网络进行数据的收发 13 assert(sockfd!=-1); 14 15 struct sockaddr_in saddr,caddr;//定义端口地址结构体 16 memset(&saddr,0,sizeof(saddr)); 17 saddr.sin_family=AF_INET;//IPV4 18 saddr.sin_port=htons(6000);//端口:转成网络端口(大端) 19 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//地址(字符串转整形) 20 //指定套接字的ip,port 绑定 21 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); 22 assert(res!=-1); 23 //设置监听队列的大小:5 24 res=listen(sockfd,5); 25 assert(res!=-1); 34 while(1) 45 { 23 int len=sizeof(caddr); 26 int c=accept(sockfd,(struct sockaddr*)&caddr,&len); 27 if(c<0) 28 { 29 continue 30 } 31 printf("accept c=%d\n",c) 32 pthread_t id; 33 pthread_create(&idd,NULL,recv_thread,(void*)c);//创建并开始线程 34 } 35 }
多进程实现TCP
优点:每个进程具有独立性,一个进程存在问题不会影响到其他的进程。
缺点:开销较大。
lcx@lcx-virtual-machine:~/mycode/1.8$ vi ser.c 1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 #include 9 #include 9 void fun(int sig)//利用信号处理僵死进程 9 { 8 wait(NULL); 9 } 10 int main() 11 { 12 signal(SIGCHLD,fun); 13 //signal(SIGCHLD,SIG_IGN);//UNIXx 12 int sockfd =socket(AF_INET,SOCK_STREAM,0);//创建套接字,他能通过网络进行数据的收发 13 assert(sockfd!=-1); 14 15 struct sockaddr_in saddr,caddr;//定义端口地址结构体 16 memsetu(&saddr,0,sizeof(saddr)); 17 saddr.sin_family=AF_INET;//IPV4 18 saddr.sin_port=htons(6000);//端口:转成网络端口(大端) 19 saddr.sin_addr.s_addr=inet_addr("127.0.0.1");//地址(字符串转整形) 20 //指定套接字的ip,port 绑定 21 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); 22 assert(res!=-1); 23 //设置监听队列的大小:5 24 res=listen(sockfd,5); 25 assert(res!=-1); 26 while(1) 27 { 28 int len=sizeof(caddr); 29 int c=accept(sockfd,(struct sockaddr*)&caddr,&len);//接受链接,如果监听队列为空阻塞住,仅能处理已完成三次握手的监听队列 30 //c是链接套接字,sockfd 是监听套接字 31 if(c<0) 32 { 33 continue;//不成功重新接收 34 } 35 printf("accept c=%d\n",c); 36 pid_t pid=fork();//fork产生一个新进程 37 if(pid == -1)//fork失败 38 { 39 close(c); 40 printf("fork child err\n"); 40 continue; 41 } 46 if(pid==0)//子进程,复制了c 47 { 49 while(1) 48 { 50 char buff[128]={0}; 49 int n=recv(c,buff,127,0);//接收客户端数据 48 if(n==0) 48 { 47 break; 49 } 40 printf("child read:%s\n",buff);//打印 41 send(c,"ok",2,0);//给客户端恢复OK 49 } 51 printf("client close\n"); 50 close(c); 48 exit(0); 49 } 50 close(c);//父进程,什么都不干,关掉c 51 } 50 }
观察TCP流程
lcx@lcx-virtual-machine:~/桌面$ sudo su root@lcx-virtual-machine:/home/lcx/桌面# apt install net-tools 正在读取软件包列表... 完成 正在分析软件包的依赖关系树 正在读取状态信息... 完成 net-tools 已经是最新版 (1.60+git20180626.aebd88e-1ubuntu1)。 升级了 0 个软件包,新安装了 0 个软件包,要卸载 0 个软件包,有 286 个软件包未被升级。 root@lcx-virtual-machine:/home/lcx/桌面# exit
命令:netstat -natp;
netstat -ncatp(自动刷新);
lcx@lcx-virtual-machine:~/桌面$ netstat -natp (并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户) 激活Internet连接 (服务器和已建立连接的) // 协议 接收缓冲 发送缓冲 本地地址及 外接地址 TCP 进程PID TCP 区字节数 区字节数 端口号 及端口号 连接状态 j // Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN - tcp 0 0 127.0.0.1:631 0.0.0.0:* LISTEN - tcp 0 0 192.168.74.128:46166 151.101.74.49:443 ESTABLISHED 4098/./ser tcp6 0 0 ::1:631 :::* LISTEN - lcx@lcx-virtual-machine:~/桌面$
服务器端或者客户端谁先关闭,谁最后停留在timewait状态,二者同时关闭时,同时发送fin,最后都进入timewait状态
UDP编程流程
基础概念
UDP 协议:无连接,不可靠,数据报
不可靠:不会检查应答,自顾自发。
数据报服务:发送的数据报是一个整体,一个数据报只会接受一次,一次必须接收整个数据报,否则就会将这个数据报中未接受的数据丢掉。
效率性能高于TCP,可靠性低于TCP
服务器端 | 客户端 |
---|---|
socket() | socket() |
bind() | //bind() |
recvform() | sendto() |
sendto() | recvfrom() |
close() | close() |
socket()用来创建套接字,使用 udp 协议时,选择数据报服务 SOCK_DGRAM。
sendto() 用来发送数据,由于 UDP 是无连接的,每次发送数据都需要指定对端的地址(IP 和端 口)。
recvfrom()接收数据,每次都需要传给该方法一个地址结构来存放发送端的地址。
recvfrom()可以接收所有客户端发送给当前应用程序的数据,并不是只能接收某一个客 户端的数据。
udp协议多用于实时传输数据,不介意丢数据问题
UDP 数据报服务特点:发送端应用程序每执行一次写操作,UDP 模块就将其封装成一 个 UDP 数据报发送。接收端必须及时针对每一个 UDP 数据报执行读操作,否则就会丢包。 并且,如果用户没有指定足够的应用程序缓冲区来读取 UDP 数据,则 UDP 数据将被截断。
模拟实现UDP
服务器端
lcx@lcx-virtual-machine:~/mycode/1.24$ vi udpser.c 1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 int main() 11 { 12 int sockfd =socket(AF_INET,SOCK_DGRAM,0); 13 assert(sockfd!=-1); 14 struct sockaddr_in saddr,caddr; 15 memset(&saddr,0,sizeof(saddr)); 16 saddr.sin_family=AF_INET; 17 saddr.sin_port=htons(6000); 18 saddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 19 int res=bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr)); 20 assert(res!=-1); 21 while(1) 22 { 23 int len=sizeof(caddr); 24 char buff[128]={0}; 25 recvfrom(sockfd,buff,127,0,(struct sockaddr*)&caddr,&len); 26 printf("buff=%s\n",buff); 27 sendto(sockfd,"ok",2,0,(struct sockaddr*)&caddr,sizeof(caddr)); 28 } 29 } 30 lcx@lcx-virtual-machine:~/mycode/1.24$ gcc -o udpser udpser.c
客户端
lcx@lcx-virtual-machine:~/mycode/1.24$ vi udpli.c 1 #include2 #include 3 #include 4 #include 5 #include 6 #include 7 #include 8 #include 9 10 int main() 11 { 12 int sockfd =socket(AF_INET,SOCK_DGRAM,0); 13 assert(sockfd!=-1); 14 struct sockaddr_in saddr,caddr; 15 memset(&saddr,0,sizeof(saddr)); 16 saddr.sin_family=AF_INET; 17 saddr.sin_port=htons(6000); 18 saddr.sin_addr.s_addr=inet_addr("127.0.0.1"); 19 while(1) 20 { 21 char buff[128]={0}; 22 fgets(buff,128,stdin); 23 if(strncmp(buff,"end",3)==0) 24 { 25 break; 26 } 27 sendto(sockfd,buff,strlen(buff)-1,0,(struct sockaddr*)&saddr,sizeof( saddr)); 28 memset(buff ,0,128); 29 int len=sizeof(saddr); 30 recvfrom(sockfd,buff,127,0,(struct sockaddr*)&saddr,&len); 31 printf("buff=%s\n",buff); 32 } 33 close(sockfd); 34 35 } ~ ~ lcx@lcx-virtual-machine:~/mycode/1.24$ gcc -o udpcli udpli.c