前言
基于VS2015写一个socket通信的Client/Server小程序,验证TCP协议的三次握手与四次挥手。
Server:监听本机的8888端口。
Client:向本机地址(127.0.0.1:8888)发送信息。
工具
Wireshark
代码
Server代码:
#include <WinSock2.h>
#include <iostream>
using namespace std;
#pragma comment(lib,"ws2_32.lib")
const int port = 8888;
int main()
{
//初始化WSA(windows下需要用WSAStarup)
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
// 创建套接字
SOCKET sock;
sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET)
{
cout << "Socket error!" << endl;
return -1;
}
// 将地址绑定到port 和 ip
struct sockaddr_in name;
name.sin_family = AF_INET;
name.sin_port = htons(8888);
name.sin_addr.S_un.S_addr = INADDR_ANY; // 通配地址
// 绑定套接字
if (bind(sock, (LPSOCKADDR)&name, sizeof(name)) == SOCKET_ERROR)
{
cout << "bind error!" << endl;
return -1;
}
// 监听
if (listen(sock, 5) == SOCKET_ERROR)
{
cout << "listen error!" << endl;
}
// 等待连接
char data[102400] = { 0 };
SOCKET sClient;
sockaddr_in remoteAddr;
int nAddrlen = sizeof(remoteAddr); // 要初始化,否则默认为0,accept()出错
while (1)
{
cout << "等待连接..." << endl;
// 接受一个客户端的连接请求
sClient = accept(sock, (SOCKADDR*)&remoteAddr, &nAddrlen);
if (sClient == INVALID_SOCKET)
{
cout << "accept error!" << endl;
continue;
}
cout << "接受到一个连接: " << inet_ntoa(remoteAddr.sin_addr) << endl;
// 接受数据
int ret = 0;
int realSize = 0;
int length = sizeof(data);
while (1)
{
ret = recv(sClient, (char*)(data + ret), length, 0);
if (ret <= 0)
{
break;
}
length -= ret;
realSize += ret;
}
data[realSize - 1] = '\0';
cout << "接收:" << realSize << endl;
closesocket(sClient);
}
return sock;
}
Client代码:
#include <WinSock2.h>
#include <iostream>
using namespace std;
#pragma comment(lib, "ws2_32.lib")
#define SERVER_ADDRESS "127.0.0.1" //服务器端IP地址
#define PORT 8888 //服务器的端口号
const int MESSIGE_SIZE = 102400;
void Send_data(int sockfd)
{
char* query;
query = new char[MESSIGE_SIZE + 1];
for (int i = 0; i < MESSIGE_SIZE; i++)
{
query[i] = 'a';
}
query[MESSIGE_SIZE] = '\0';
size_t remaining = strlen(query);
while (remaining)
{
int n_written = send(sockfd, query, remaining, 0);
cout << "send into buffer " << n_written << endl;
if (n_written <= 0)
{
cout << "send failed" << endl;
return;
}
remaining -= n_written;
query += n_written;
}
}
int main()
{
//初始化WSA(windows下需要用WSAStarup)
WORD sockVersion = MAKEWORD(2, 2);
WSADATA wsaData;
if (WSAStartup(sockVersion, &wsaData) != 0)
{
return 0;
}
// 创建套接字和服务器地址
int sockfd;
sockaddr_in server_addr;
// 初始化套接字和服务器地址
sockfd = socket(PF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS);
// 建立连接
if ( connect(sockfd, (sockaddr*)&server_addr, sizeof(server_addr)) == SOCKET_ERROR )
{
cout << "连接失败" << endl;
closesocket(sockfd);
}
// 发送数据
Send_data(sockfd);
closesocket(sockfd);
return 0;
}
抓包
工具使用方法:
- 接口:Adapter for loopback traffic capture(适于本机)
- 过滤器:tcp.port == 8888 and (ip.src == 127.0.0.1 or ip.dst == 127.0.0.1)
抓包结果:
分析
三次握手(详细可参考:https://blog.csdn.net/qq_34386891/article/details/80515912)
发送 | 接收 | 标志位 | 同步信息 | 补充 |
---|---|---|---|---|
Client | Server | SYN=1 | seq=0, len=0 | 发送自己的序号 |
Server | Client | ACK=1, SYN=1 | ack=1, seq=0, len=0 | 回复的ack=Client的seq + 1,并发送自己的序号 |
Client | Server | ACK=1 | ack=1, seq=1, len=0 | 发送自己的序号,自己的序号就是对面回复的ack |
发送数据
本例Client向Server发送120400个字节数据,client选的是阻塞式发送,向缓冲区发送102400字节,缓冲区装不下了会阻塞在send函数,Server读取了内容,缓冲区空出空间就可以继续向缓冲区写数据。本例的102400字节,向缓冲区写了2次才写完。
发送 | 接收 | 标志位 | 同步信息 | 补充 |
---|---|---|---|---|
Client | Server | ACK=1 | ack=1, seq=1, len=65495 | 回复对方的下一帧序号,发送自己的序号、数据长度 |
Server | Client | ACK=1 | ack=65496, seq=1, len=0 | 回复的ack=Client的seq + Client的len,并发送自己的序号、数据长度 |
Client | Server | PSH=1,ACK=1 | ack=1, seq=65496, len=36905 | PSH=1,提醒Server尽快取走报文; 回复的ack=Server的seq + Server的len,并发送自己的序号、数据长度 |
Server | Client | ACK=1 | ack=102401, seq=1, len=0 | 回复的ack=Client的seq + Client的len,并发送自己的序号、数据长度 |
四次挥手
发送 | 接收 | 标志位 | 同步信息 | |
---|---|---|---|---|
Client | Server | FIN=1,ACK=1 | ack=1, seq=102401, len=0 | 回复对方的下一帧序号,发送自己的序号 |
Server | Client | ACK=1 | ack=102402, seq=1, len=0 | 回复的ack=Client的seq + 1,并发送自己的序号 |
Server | Client | FIN=1,ACK=1 | ack=102402, seq=1, len=0 | 回复对方下一帧的序号,发送自己的序号 |
Client | Server | ACK=1 | ack=2, seq=102402, len=0 | 回复的ack=Server的seq + 1,并发送自己的序号 |
总结
- 在握手和挥手阶段,ack=seq+1;
- 在发送数据阶段,ack=seq+len;
- 当发送缓冲区占满,下一帧数据会置PSH=1,提醒接收端尽快取走数据。