1.TCP 三次握手与四次挥手
1.1 首先需要了解几个名词
-
- 序列号seq:占4个字节,用来标记数据段的顺序,TCP把连接中发送的所有数据字节都编上一个序号,第一个字节的编号由本地随机产生;给字节编上序号后,就给每一个报文段指派一个序号;序列号seq就是这个报文段中的第一个字节的数据编号;
- 2.确认号ack:占4个字节,期待收到对方下一个报文段的第一个数据字节的序号;序列号表示报文段携带数据的第一个字节的编号;而确认号指的是期望接收到下一个字节的编号;因此当前报文段最后一个字节的编号+1即为确认号;
- 3.确认ACK:占1位,仅当ACK=1时,确认号字段才有效;ACK=0时,确认号无效;
- 4.同步SYN:连接建立时用于同步序号;当SYN=1,ACK=0时表示:这是一个连接请求报文段;若同意连接,则在响应报文段中使得SYN=1,ACK=1;因此,SYN=1表示这是一个连接请求,或连接接受报文;SYN这个标志位只有在TCP建产连接时才会被置1,握手完成后SYN标志位被置0;
- 5.终止FIN:用来释放一个连接。FIN=1表示:此报文段的发送方的数据已经发送完毕,并要求释放运输连接;
1.2 ACK、SYN和FIN这些大写的单词表示标志位,其值要么是1,要么是0;ack、seq小写的单词表示序号;
1.3 三次握手步骤
- 1.第一次握手:客户端发送意图连接请求,SYN=1,seg=x;客户端进入SYN_SENT(同步已发送)状态,等待服务器确认;
- 2.第二次握手:服务器端发送确认连接消息,SYN=1,ACK=1,ack=x+1,seg=y;服务器端由LISTEN状态进入SYN_RECV(同步接受)状态;
- 3.第三次握手:客户端发送确认包数据,ACK=1,seg=x+1,ack=y+1;服务器和客户端都进入ESTABLISHED(TCP连接成功)状态;
1.4 四次挥手步骤
- 1.第一次挥手:客户端发送释放连接请求报文,FIN=1,ACK=1,seg=X,ack=z(这个ack是上一次收到数据时的确认序号+1);客户端进入FIN_WAIT_1状态;
- 2.第二次挥手:服务器收到释放连接报文,发送确认报文,ACK=1,seg=Y,ack=X+1;服务器进入CLOSE_WAIT状态;这个时候客户端仍是可以接收数据的,因为若第一次挥手之前的某个数据包未收到仍需客户端再传一次;
- 3.第二次挥手与第三次挥手之间客户端的状态变化:客户端收到服务器的确认报文后进入FIN_WAIT_2状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据);
- 4.第三次挥手:服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,ACK=1,seg=YY,ack=X+1;服务器进入LAST_ACK(最后确认)状态;
- 5.第四次挥手:客户端收到服务器的连接释放报文之后,发送确认报文,ACK=1,seg=X+1,ack=YY+1;此时客户端进入TIME_WAIT(时间等待)状态,此时TCP连接并没有断开,仍需经过两个最长报文寿命的时间后才进入CLOSE状态;但服务器只要收到客户端发送的确认报文之后立即进入CLOSE状态。
1.5 为什么握手要三次,挥手却要四次?
- 因为当建立连接时 Server 只要接受到 Client 的连接请求尽能够进行发送数据;而断开连接时 Server 收到 FIN 后可能仍有数据未发送,因此收到 FIN 后只会通知 Client 收到了 FIN ,但并不会立即关闭,仍需 Server 在报文发送完毕之后一次确认才会断开,因此会比建立连接多一次。
1.6 能不能将三次握手改为两次?
- 不能。因为三次握手完成了两件事:1)双方做好数据发送的准备,2)确认数据传输过程中的初始序号;若只使用两次握手,那么当第二次握手时的数据包丢失的话,客户端会一直等待确认连接建立的数据包,而忽略服务器端认为连接建立成功而发送的数据,这样就会形成死锁。
1.7 丢包
- 1.丢包分两种情况:1)发送的数据包丢了,2)回复包丢了;
- 2.解决回复包丢失的方法:一次多发几个数据包,按照最后一个回复包来进行第二次数据发送;
- 3.解决发送包丢失的方法:服务器端丢了第几个包就一直发送丢了的包的起始序号;
1.8 滑动窗口
- 1.滑动窗口(Sliding Window)是一种流量控制技术,是为了解决同时发送数据时发生的网络阻塞进而导致所有的发送端都无法发送数据的现象;
- 2.滑动窗口协议是用来改善吞吐量的一种技术,即容许发送方在接收任何应答之前传送附加的包。接收方告诉发送方在某一时刻能送多少包(称窗口尺寸);
- 3.超时重传:在发送一个数据包之后就打开一个计时器,在一定时间内没有得到发送的数据报的ACK报文,那么就重新发送数据,知道发送成功为止。
2.使用TCP/IP协议进行文件的传输
2.1 创建一个新的解决方案
- 1.新建一个 Client 项目,仍是之前的步骤:加载库->创建套接字->绑定本地地址->等待链接成功->发送数据->关闭套接字->卸载库;
- 2.我们需要改写发送数据部分的代码;
- 3.创建一个结构体用来存放传输的文件信息: filename 以及 filelen ;
- 4.在与当前 cpp 文件同级的目录下加入一个待传输文件,我们将要传输这个文件;创建一个结构体对象,通过 strcpy_s 函数将文件名赋值给 filename ;发送这个结构体;
- 5.当成功发送这个文件的信息后,我们接受服务器端的返回信息,若服务器同一接受文件,则我们开始一直发送文件;
#include <iostream>
using namespace std;
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
#include "io.h"
int main()
{
// 1.加载库
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
// 2.创建监听套接字
SOCKET client_socket = ::socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(client_socket == INVALID_SOCKET)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(client_socket);
client_socket = 0;
WSACleanup();
return -1;
}
// 3.将监听套接字与本机进行绑定
sockaddr_in addr_client;
addr_client.sin_family = AF_INET;
addr_client.sin_addr.S_un.S_addr = INADDR_ANY; // 连接本地任意网卡
addr_client.sin_port = htons(4568);
if(::bind(client_socket, (const sockaddr*)&addr_client, sizeof(sockaddr_in)) == SOCKET_ERROR)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(client_socket);
client_socket = 0;
WSACleanup();
return -1;
}
// 5.监听成功进行连接
sockaddr_in addr_server;
addr_server.sin_family = AF_INET;
addr_server.sin_port = htons(4567);
addr_server.sin_addr.S_un.S_addr = inet_addr("192.168.3.25");
if(::connect(client_socket, (const sockaddr*)&addr_server, sizeof(sockaddr_in)) == SOCKET_ERROR)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(client_socket);
client_socket = 0;
}
else
{
cout << "连接成功。。。" << endl;
}
// =========================3-19============================
// 1.获取文件信息
// 先定义一个存放文件信息的结构体
struct FileInfo
{
char file_name[50];
int file_len;
};
FileInfo fileinfo;
strcpy_s(fileinfo.file_name, 50, "CMake Practice.pdf");
FILE* fp = 0;
fopen_s(&fp, "CMake Practice.pdf", "rb"); // 以读写的形式打开文件
int no = fileno(fp);
fileinfo.file_len = filelength(no);
// 2.发送文件信息
int n_send_size = ::send(client_socket, (const char*)&fileinfo, sizeof(fileinfo), 0);
if(n_send_size <= 0)
cout << "error code: " << WSAGetLastError() << endl;
else
{
// 3.接受对方回应信息
bool b_flag = false;
int n_recv_size = ::recv(client_socket, (char*)&b_flag, sizeof(bool), 0);
if(n_recv_size <= 0)
cout << "error code: " << WSAGetLastError() << endl;
else if(b_flag == true)
{
// 4.如果对方同意接受,则不停地发送文件
}
}
// =========================3-19============================
::closesocket(client_socket);
client_socket = 0;
WSACleanup();
return 0;
}
2.2 再创建一个服务器端项目
- 1.对于服务器端,我们仍然:加载库->创建套接字->绑定本地地址->监听->连接成功->收发数据->关闭套接字->卸载库;
- 2.改写收发数据处的代码;
- 3.也需要一个和客户端相同的存文件信息的结构体,通过这个结构体来存放服务器接收到的信息,并将接收到的信息打印;
- 4.当确认接受到文件信息之后,从键盘输入一个是否接受文件,若是则接收文件,打开一个新文件进行写入,否则退出;
#include <iostream>
using namespace std;
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
printf("WSAStartup failed with error: %d\n", err);
return 1;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("Could not find a usable version of Winsock.dll\n");
WSACleanup();
return 1;
}
else
printf("The Winsock 2.2 dll was found okay\n");
// 2.创建监听套接字
SOCKET listen_socket = ::socket(AF_INET, SOCK_STREAM , IPPROTO_TCP);
if(listen_socket == INVALID_SOCKET)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(listen_socket);
listen_socket = 0;
WSACleanup();
return -1;
}
// 3.将监听套接字与本机进行绑定
sockaddr_in addr_server;
addr_server.sin_family = AF_INET;
addr_server.sin_addr.S_un.S_addr = INADDR_ANY; // 连接本地任意网卡
addr_server.sin_port = htons(4567);
if(::bind(listen_socket, (const sockaddr*)&addr_server, sizeof(sockaddr_in)) == SOCKET_ERROR)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(listen_socket);
listen_socket = 0;
WSACleanup();
return -1;
}
// 4.监听
if(::listen(listen_socket, 64) == SOCKET_ERROR)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(listen_socket);
listen_socket = 0;
WSACleanup();
return -1;
}
// 5.监听成功进行连接
cout << "开始监听。。。。。。。。。。。" << endl;
sockaddr_in addr_client;
int n_size_addr = sizeof(addr_client);
SOCKET socket_client = ::accept(listen_socket, (sockaddr*)&addr_client, &n_size_addr);
if(INVALID_SOCKET == socket_client)
{
cout << "error code: " << WSAGetLastError() << endl;
::closesocket(socket_client);
socket_client = 0;
}
cout <<"监听成功。。。" << inet_ntoa(addr_client.sin_addr) << endl;
// =========================3-19==========================
// 1.连接成功则进行接收文件
struct FileInfo
{
char file_name[50];
int file_len;
};
FileInfo fileinfo;
int n_recv_size = ::recv(socket_client, (char*)&fileinfo, sizeof(fileinfo), 0);
if(n_recv_size <= 0)
cout << "error code: " << WSAGetLastError() << endl;
else
{
cout << fileinfo.file_name << " size:" << fileinfo.file_len;
bool b_flag = false;
cout << "是否要接受文件?(0/1)" << endl;
cin >> b_flag;
// 2.接受一个输入判断是否进行接受
// 3.无论是否进行接受都需要给对方一个回复,send
// 4.如果要接受,打开一个新的文件
// while()
// recv , fwrite
}
// =========================3-19==========================
::closesocket(listen_socket);
listen_socket = 0;
WSACleanup();
return 0;
}
参考:https://blog.csdn.net/qq_38950316/article/details/81087809