【1】TCP编程
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>int main(int argc, char const *argv[])
{char buf[128] = {0};int ret = 0;// 1.创建套接字(socket)int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}printf("sockfd:%d\n", sockfd);// 2.指定网络信息struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(5678); // 端口号saddr.sin_addr.s_addr = inet_addr("192.168.50.81"); // 虚拟机IP地址// 3.连接服务器if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("connect er");close(sockfd);return -1;}printf("connect okk\n");// 4.接收发送消息(send recv)while (1){fgets(buf, sizeof(buf), stdin);if (buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';// write(sockfd, buf, sizeof(buf));if (!strcmp(buf, "quit"))break;send(sockfd, buf, sizeof(buf), 0);memset(buf, 0, sizeof(buf));}// 8.关闭套接字(close)close(sockfd);return 0;
}
1. 优化
1. 优化服务器代码,客户端链接成功后,可以循环多次通信,当客户端输入quit时,客户端退出。
2. 优化服务器代码客户端输入quit退出后,服务器不退出,等待下一个客户端连接
循环服务器:一个服务器可以连接多个客户端,但是不能同时连接
3. 地址和端口都通过参数传入
4. 自动获取本机地址
5. 增加来电显示功能:显示客户端连入时的IP和port
2. 服务器
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{char buf[128] = {0};int ret = 0;// 1.创建套接字(socket)int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}printf("sockfd:%d\n", sockfd);// 2.指定网络信息struct sockaddr_in saddr, caddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[1])); // 端口号// saddr.sin_addr.s_addr = inet_addr("192.168.50.81"); // 虚拟机IP地址// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");// INADDR_ANY:32位的无符号数saddr.sin_addr.s_addr = INADDR_ANY;int len = sizeof(caddr);// 3.绑定套接字(bind)if (bind(sockfd, (struct sockaddr *)&saddr,sizeof(struct sockaddr_in)) != 0){perror("bind err");return -1;}printf("bind okk\n");// 4.监听(listen)将主动套接字变为被动套接字if (listen(sockfd, 6) != 0){perror("listen errr");return -1;}printf("listen okk\n");// 队列1:保存正在连接// 队列2,连接上的客户端// 5.接收客户端连接请求(accept)while (1){int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &len);// 返回用于通信的文件描述符// 在tcp服务器中,有两类文件描述符// 1.socket函数的返回值:一个用于连接的文件描述符// 2.accept函数的返回值:一个或多个用于通信的文件描述符if (acceptfd < 0){perror("accept err");return -1;}printf("acceptfd:%d\n", acceptfd);printf("IP:%s port:%d\n",inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));// 6.接收发送消息(send recv)while (1){ret = recv(acceptfd, buf, sizeof(buf), 0);if (ret < 0){perror("recv err");return -1;}else if (ret == 0){printf("client exit\n");break;}// read(acceptfd,buf,sizeof(buf));printf("buf:%s\n", buf);memset(buf, 0, sizeof(buf));}// 7.关闭套接字(close)close(acceptfd);}close(sockfd);return 0;
}
3. 客户端
#include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char const *argv[])
{if(argc!=3){printf("please input %s <ip> <port>\n",argv[0]);return -1;}char buf[128] = {0};int ret = 0;// 1.创建套接字(socket)int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket err");return -1;}printf("sockfd:%d\n", sockfd);// 2.指定网络信息struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(atoi(argv[2])); // 端口号saddr.sin_addr.s_addr = inet_addr(argv[1]); // 虚拟机IP地址// 3.连接服务器if (connect(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0){perror("connect er");close(sockfd);return -1;}printf("connect okk\n");// 4.接收发送消息(send recv)while (1){fgets(buf, sizeof(buf), stdin);if (buf[strlen(buf) - 1] == '\n')buf[strlen(buf) - 1] = '\0';// write(sockfd, buf, sizeof(buf));if (!strcmp(buf, "quit"))break;send(sockfd, buf, sizeof(buf), 0);memset(buf, 0, sizeof(buf));}// 8.关闭套接字(close)close(sockfd);return 0;
}
【2】FTP项目
模拟FTP核心原理:客户端连接服务器后,向服务器发送一个文件。文件名可以通过参数指定,服务器端接收客户端传来的文件(文件名随意),如果文件不存在自动创建文件,如果文件存在,那么清空文件然后写入。
项目功能介绍:
均有服务器和客户端代码,基于TCP写的。
在同一路径下,将客户端可执行代码复制到其他的路径下,接下来在不同的路径下运行服务器和客户端。
相当于另外一台电脑在访问服务器。
客户端和服务器链接成功后出现以下提示:三个功能
***********put filename**********//从客户端所在路径上传文件
***********get filename**********//从服务器所在路径下载文件
**************quit***************//退出(可只退出客户端,服务器等待下一个客户端链接)
【3】TCP粘包
tcp粘包
tcp拆包
TCP粘包、拆包发生原因:
发生TCP粘包或拆包有很多原因,常见的几点:
1、要发送的数据大于TCP发送缓冲区剩余空间大小,将会发生拆包。
2、待发送数据大于MSS(传输层的最大报文长度),将进行拆包.
3、要发送的数据小于TCP发送缓冲区的大小,TCP将多次写入缓冲区的数据一次发送出去,将会发生粘包。
4、 接收数据端的应用层没有及时读取接收缓冲区中的数据,将发生粘包。
粘包解决办法:
解决问题的关键在于如何给每个数据包添加边界信息,常用的方法有如下:
1、发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。
2、发送端将每个数据包封装为固定长度,这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。
3、可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。
4、延时发送
【4】Wireshark:抓包工具
1. 安装
win:关闭防火墙,安装包默认下载即可
linux:sudo apt-get install wireshark
2. 启动
win:
双击打开
linux:
sudo wireshark
3. 选择网卡
win:
linux:
4. 过滤包
1. ip.addr == x.x.x.x:只显示源或目标IP地址为x.x.x.x的数据包。
2. tcp.port == x:只显示源或目标端口号为x的TCP数据包。
3. udp.port == x:只显示源或目标端口号为x的UDP数据包。
4. ip.src == x.x.x.x:只显示源IP地址为x.x.x.x的数据包。
5. ip.dst == x.x.x.x:只显目标IP地址为x.x.x.x的数据包。
【5】网络协议头分析
数据的封装与传递过程
思考:
1. 应用层调用send后,是如何把数据发送到另一台机器的某个进程的。
2. 接收的设备收到数据包后,如何处理给应用层?
思考:在协议栈封装的过程中,这些头部信息具体有什么呢?
以太网帧完整帧
● 对于网络层最大数据帧长度是1500字节
● 对于链路层最大数据长度是1518字节(1500+14+CRC)
● 发送时候,IP层协议栈程序检测到发送数据和包头总长度超过1500字节时候,会进行自动分包处理,接收端在IP层进行包重组,然后才继续往上传递
【6】三次握手与四次挥手
三次握手
第一次握手都由客户端发起
在TCP/IP协议中,TCP协议提供可靠的连接服务,采用三次握手建立一个连接。
服务器必须准备好接受外来的连接。这通过调用socket、 bind和listen函数来完成,称为被动打开(passive open)。listen
第一次握手:客户通过调用connect进行主动打开(active open)。这引起客户TCP发送一个SYN(表示同步)分节(SYN=J),它告诉服务器客户将在连接中发送数据的初始序列号。并进入SYN_SEND状态,等待服务器的确认。
第二次握手:服务器必须确认客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器以单个字节向客户发送SYN和对客户SYN的ACK(表示确认),此时服务器进入SYN_RECV状态。
第三次握手:客户收到服务器的SYN+ACK。向服务器发送确认分节,此分节发送完毕,客户服务器进入ESTABLISHED状态,完成三次握手。
1. SYN_SEND:客户端发送SYN报文后进入此状态,等待服务器的确认。
2. SYN_RECV:服务器收到SYN报文后进入此状态,等待客户端的确认。
3. ESTABLISHED:当客户端和服务器端都发送和接收了ACK报文后,连接进入此状态,表示连接已经建立,可以进行数据传输。
类比打电话的过程:
第一次握手:喂,能听见我说话吧?
第二次握手:能听见你说话,你能听见我说话不?
第三次握手:能听见
开始通话
客户端的初始序列号为J,而服务器的初始序列号为K。在ACK里的确认号为发送这个ACK的一端所期待的下一个序列号。因为SYN只占一个字节的序列号空间,所以每一个SYN的ACK中的确认号都是相应的初始序列号加1.类似地,每一个FIN(表示结束)的ACK中的确认号为FIN的序列号加1.
完成三次握手,客户端与服务器开始传送数据,在上述过程中还有一些重要概念。
未连接队列:在三次握手协议中,服务器维护一个未连接队列,该队列为每个客户端的SYN包(syn=j)开设一个条目,该条目表明服务器已收到SYN包,并向客户发出确认,正在等待客户端确认包。这些条目所标识的连接在服务器处于SYN_RECV状态,当服务器收到客户端确认包时,删除该条目,服务器进入ESTABLISHED状态。
发送数据包一定会有发送序号,但是不一定有确认序号,只有在发送的数据包中有ACK包的时候才会有确认序号(应答号)
面试题:
tcp在建立连接的过程中会涉及到哪些状态的变换--》
tcp的连接过程--》三次握手
tcp的三次握手会发送在哪两个函数之间--》connect accept
为什么一定是三次握手,不能是两次握手--》主要是为了防止已经失效的连接请求报文突然又传送到了服务器,从而导致不必要的错误和资源的浪费。
两次握手只能保证单向连接是畅通的。因为TCP是一个双向传输协议,只有经过第三次握手,才能确保双向都可以接收到对方的发送的数据。
ACK:确认包
PSH:数据包
SYN:同步包(握手包)
seq:序列号
ack:确认号
四次挥手
四次挥手既可以由客户端发起,也可以由服务器发起
TCP连接终止需四个分节。
类比挂电话的过程:
第一次挥手:我说完了,我要挂了
第二次挥手:好的,我知道了,但是你先别急,等我把话说完
第三次挥手:好了,我说完了,咱们可以挂电话了
第四次挥手:好的,挂了吧
1MSL:数据包在系统内的最大存活时间
第一次挥手:某个应用进程首先调用close,我们称这一端执行主动关闭。这一端的TCP于是发送一个FIN分节,表示数据发送完毕。
第二次挥手:接收到FIN的另一端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为文件结束符传递给接收端应用进程(放在已排队等候应用进程接收到任何其他数据之后)
第三次挥手:一段时间后,接收到文件结束符的应用进程将调用close关闭它的套接口。这导致它的TCP也发送一个FIN。
第四次挥手:接收到这个FIN的原发送端TCP对它进行确认。
第一次挥手:主动断开方向被动断开方发送FIN挥手包,表示自己发送完毕
第二次挥手:被动断开方接收到FIN之后给主动断开方回复ACK
第三次挥手:被动断开方向主动断开方发送FIN挥手包,表示自己也发送完毕。
第四次挥手:主动断开方接收到FIN之后给被动断开方回复ACK,表示确认关闭连接。
面试题
描述一下四次挥手
第二次挥手与第三次挥手之间有一段时间间隔是为什么?
第四次挥手之后主动断开方会等待一段时间再关闭,这个等待的时间是多少?为什么要等待? 等待时间为2MSL,1MSL是报文在系统内的最大存活时间,等待2MSL的时间是为了确保ACK包到达被动断开方
在第一个MSL时间内,是被动断开方等待主动断开方ACK报文,若没有在1msl的时间内收到,会超时重传FIN报文,主动断开方在剩余的1MSL时间,接收到被动断开方发送的新的FIN
【7】服务器模型
在网络通信中,通常要求一个服务器连接多个客户端
为了处理多个客户端的请求,通常有多种表现形式
1.循环服务器:一个服务器可以连接多个客户端,但是不能同时连接
socket();
bind();
listen();
while(1)accept();while(1)recv();/send();
close();
2. 并发服务器:一个服务器可以在同一时间处理多个客户端的请求
1. 多线程:每有一个客户端连接就创建一个线程通信
为什么要创建多线程---》通信
什么时间创建多线程---》accept之后创建线程
子线程:通信
主线程:循环等待下一个客户端连接