目录
0x00 多客户端简单聊天室
思路:
主线程循环不断接受客户端连接:
- 创建一个全局数组来保存每次连接主套接字产生的从套接字(为什么要全局数组,为了线程之间通信)
- 循环调用accept函数接受客户端的连接
每个客户端连接上成功后,创建一个线程来负责和这个客户端通信
客户端发送给服务器的数据,服务器接受后需要转发给当前连接上服务器的所有客户端
客户端需要有两个线程:
- 一个线程循环不断接受用户输入,并发送给服务器
- 另一个线程循环不断接受服务器发来的数据并显示
代码:
服务器代码:
// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
#define MAXCLIENT 1024
//全局数组 保存主套接字管辖的众多子套接字
SOCKET connClient[MAXCLIENT];
//全局变量 保存当前连接成功的客户端数量
int clientNum = 0;
//用来和客户端通信的线程函数
//@param index 子套接字在connClient数组中的索引
void Chat(LPVOID index) {
int idx = (int)index;
char buff[1024];
char temp[1024];
int r;
while (1) {
r = recv(connClient[idx], buff, 1023, NULL);
//NULL表示以阻塞方式接受
//也就说当recv返回时,只有两种可能
//r > 0 接收到的数据字节数>0,即接受到了客户端的数据
//r < 0 客户端已经断开了连接
if (r > 0) {
buff[r] = 0;//添加字符串结束符号 '\0'
printf("%d:>>%s\n", idx, buff);
//广播给所有连接上服务器的客户端
for (int i = 0; i < clientNum; i++) {
memset(temp, 0, 1024);
sprintf(temp, "%d:%s", idx, buff);//来自idx的buff
send(connClient[i], temp, strlen(temp), NULL);
}
}
}
}
int main()
{
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建tcp socket
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SOCKET_ERROR == serverSocket) { //如果返回值为-1
printf("创建tcp套接字失败:%d\n",GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 创建协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//4 绑定
int r = bind(serverSocket,(sockaddr*) &serverAddr, sizeof serverAddr);
if (-1 == r) {
printf("绑定失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
printf("绑定成功!\n");
//5 监听
r = listen(serverSocket, 10);
if (-1 == r) {
printf("监听失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
printf("监听成功!\n");
//6 接收客户端连接
SOCKADDR_IN clientAddr = { 0 }; //用于接收客户端的协议地址族(就是控制信息)
int len = sizeof clientAddr; //用于接收客户端协议地址族信息的大小
for (int i = 0; i < MAXCLIENT; i++) {
connClient[i] = accept(serverSocket, (sockaddr*)&clientAddr, &len);
//SOCKET connClient = accept(serverSocket, NULL, NULL); //如果不想存储客户端的协议地址族信息,可以传NULL地址
if (SOCKET_ERROR == connClient[i]) {
printf("接收客户端连接失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
//inet_ntoa() 将32位unisigned int 转化为点分十进制的string
printf("有客户端连接到服务器了:%s:%d!\n", inet_ntoa(clientAddr.sin_addr), clientAddr.sin_port);
clientNum++;
CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)Chat, (LPVOID)i, NULL, NULL);
}
//8 关闭socket
closesocket(serverSocket);
//9 清除协议版本
WSACleanup();
while (1);
return 0;
}
客户端代码:
// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
#include <graphics.h>
//声明一个全局的socket 为了线程之间共享
SOCKET clientSocket;
//从服务器接受数据并显示到客户端
void recvAndShow() {
char buff[1024] = { 0 };
int r;
int idx;
char message[1024] = { 0 };
char currentTime[1024];
SYSTEMTIME sysTime;
int n=0; //计数 第几条信息
while (1) {
r = recv(clientSocket, buff, 1023, NULL);
//接受到服务器端传来的数据格式: idx:数据
if (r > 0) {
buff[r] = 0;
//从buff中拆分出idx和数据
memset(message, 0, sizeof message);
sscanf(buff, "%d:%s", &idx, message); //sscanf 从数组中拆分格式化的数据
//显示idx 显示当前时间
GetLocalTime(&sysTime); //获取当前系统时间
sprintf(currentTime, "%d %04d/%02d%02d %02d:%02d:%02d",
idx, sysTime.wYear, sysTime.wMonth, sysTime.wDay,
sysTime.wHour, sysTime.wMinute, sysTime.wSecond);
outtextxy(0, 40 * n, currentTime);
//显示数据
outtextxy(10, 40 * n + 20, message);
n++;
}
}
}
int main()
{
initgraph(200, 400, SHOWCONSOLE);
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建tcp socket
clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SOCKET_ERROR == clientSocket) { //如果返回值为-1
printf("创建tcp套接字失败:%d\n", GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 配置服务器协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//4 连接服务器
int r;
r = connect(clientSocket, (sockaddr*)&serverAddr, sizeof serverAddr);
if (SOCKET_ERROR == r) {
printf("连接服务器失败\n");
return -1;
}
printf("连接服务器成功!\n");
//4.创建一个线程专门负责从服务器接受数据并显示
HANDLE h1 = CreateThread(NULL, NULL,(LPTHREAD_START_ROUTINE)recvAndShow, NULL, NULL,NULL);
//5 向服务器发送数据
char buff[1024];
while (1) {
memset(buff, 0, 1024);
printf("请输入要发送的数据:");
scanf("%s", buff);
r = send(clientSocket, buff, strlen(buff),NULL);
if (r > 0) {
printf("发送%d字节数据成功!\n", r);
}
else {
printf("发送失败!\n");
}
}
//结束recvAndShow线程
TerminateThread(h1, 1);
//6 关闭socket
closesocket(clientSocket);
//7 清除协议版本
WSACleanup();
while (1);
return 0;
}
0x01 实现文件传输
思路:
接受端:
1.接受文件名
2.接受文件大小
3.创建文件
4.循环接受文件内容
5.关闭保存
// Server.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
int main()
{
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建tcp socket
SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SOCKET_ERROR == serverSocket) { //如果返回值为-1
printf("创建tcp套接字失败:%d\n", GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 创建协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//4 绑定
int r = bind(serverSocket, (sockaddr*)&serverAddr, sizeof serverAddr);
if (-1 == r) {
printf("绑定失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
printf("绑定成功!\n");
//5 监听
r = listen(serverSocket, 10);
if (-1 == r) {
printf("监听失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
printf("监听成功!\n");
//6 接收客户端连接
SOCKADDR_IN clientAddr = { 0 }; //用于接收客户端的协议地址族(就是控制信息)
int len = sizeof clientAddr; //用于接收客户端协议地址族信息的大小
SOCKET connClient = accept(serverSocket, (sockaddr*)&clientAddr, &len);
//SOCKET connClient = accept(serverSocket, NULL, NULL); //如果不想存储客户端的协议地址族信息,可以传NULL地址
if (SOCKET_ERROR == connClient) {
printf("接收客户端连接失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
//inet_ntoa() 将32位unisigned int 转化为点分十进制的string
printf("有客户端连接到服务器了:%s:%d!\n", inet_ntoa(clientAddr.sin_addr), clientAddr.sin_port);
//7 通信
//7.1 接受文件名
char fileName[256] = { 0 };
r = recv(connClient, fileName, 255, NULL);
if (r > 0) {
fileName[r] = 0;
printf("接受文件名成功:%s\n",fileName);
}
//7.2 创建文件
FILE* fp = fopen(fileName, "wb");
if (NULL == fp) {
printf("创建文件失败!\n");
}
printf("创建文件成功!\n");
//7.3 接收文件大小
int fileSize;
r = recv(connClient, (char*)&fileSize, 4, NULL);
if (r > 0) {
printf("接收文件大小成功:%d\n", fileSize);
}
//这时文件一定创建好了
//7.4 循环接受文件内容
char buff[1024];
int count = 0; //记录我当前接收了多少
while (1) {
r = recv(connClient, buff, 1024, NULL);
if (r > 0) {
count += r;
fwrite(buff,1, r, fp);
if (count >= fileSize) break;
}
}
fclose(fp);
printf("文件接受完毕!\n");
//8 关闭socket
closesocket(serverSocket);
//9 清除协议版本
WSACleanup();
while (1);
return 0;
}
发送端:
1.发送文件名
2.打开文件,获取文件大小并发送
3.循环发送文件内容
4.关闭文件
// Client.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
int main()
{
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建tcp socket
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (SOCKET_ERROR == clientSocket) { //如果返回值为-1
printf("创建tcp套接字失败:%d\n", GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 配置服务器协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//4 连接服务器
int r;
r = connect(clientSocket, (sockaddr*)&serverAddr, sizeof serverAddr);
if (SOCKET_ERROR == r) {
printf("连接服务器失败\n");
return -1;
}
printf("连接服务器成功!\n");
//5 通信
char fileName[256] = { 0 };
printf("请输入要发送的文件名:");
scanf("%s", fileName);
send(clientSocket, fileName, strlen(fileName), NULL);
FILE* fp = fopen(fileName, "rb");
if (NULL == fp) {
printf("打开文件失败!\n");
return -1;
}
fseek(fp, 0, SEEK_END);//定位文件指针到文件末尾
int fileSize = ftell(fp); //返回文件指针当前位置,以字节为单位
fseek(fp, 0, SEEK_SET);//定位文件指针到文件开头
r = send(clientSocket, (char*)&fileSize,4, NULL);
if (r > 0) {
printf("发送文件大小成功:%d\n", fileSize);
}
//循环发送数据
char buff[1024];
while (1) {
r = fread(buff, 1, 1024, fp);
if (r > 0) {
send(clientSocket,buff, r, NULL);
}
else {
break;
}
}
fclose(fp);
//6 关闭socket
closesocket(clientSocket);
//7 清除协议版本
WSACleanup();
while (1);
return 0;
}
0x02 UDP套接字编程
UDP套接字编程思路:
服务器:
1.请求协议版本
2.创建socket
3.确定协议地址族
4.绑定
5.通信
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
int main()
{
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建udp socket
SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (SOCKET_ERROR == serverSocket) { //如果返回值为-1
printf("创建udp套接字失败:%d\n", GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 创建协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//4 绑定
int r = bind(serverSocket, (sockaddr*)&serverAddr, sizeof serverAddr);
if (-1 == r) {
printf("绑定失败!\n");
closesocket(serverSocket);
WSACleanup();
return -1;
}
printf("绑定成功!\n");
//5 通信
char buff[1024];
while (1) {
r = recv(serverSocket, buff, 1023, NULL);
if (r > 0) {
buff[r] = 0;
printf(">>%s\n", buff);
}
}
//8 关闭socket
closesocket(serverSocket);
//9 清除协议版本
WSACleanup();
while (1);
return 0;
}
客户端:
1.请求协议版本
2.创建socket
3.获取服务器协议地址族
4.通信
#include "pch.h"
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
#include <windows.h>
int main()
{
//1.获取版本信息
WSADATA wsaData;
//参数1:协议版本号 WORD 类型
//参数2:存储版本信息的结构体的地址
WSAStartup(MAKEWORD(2, 2), &wsaData);
//MAKEWORD是一个宏函数,传入两个字节类型的数据,整合成一个WORD类型的数据
//MAKEWORD(2,2) 就是产生一个高字节为2,低字节为2的16位的数据
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
printf("得到的版本号不正确,请求版本失败\n");
return -1;
}
else {
printf("请求版本成功\n");
}
//2 创建udp socket
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (SOCKET_ERROR == clientSocket) { //如果返回值为-1
printf("创建udp套接字失败:%d\n", GetLastError());
return -1;
}
printf("创建socket成功\n");
//3 配置服务器的协议地址族 (配置控制信息)
SOCKADDR_IN serverAddr = { 0 };
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.S_un.S_addr = inet_addr("192.168.1.107"); //将点分十进制转化32位unsigned int
serverAddr.sin_port = htons(10001); //因为我们的主机是小端系统,路由器交换机是大端系统,所以我们发给大端系统的端口号应该小端转大端
//5 通信
int r;
char buff[1024];
while (1) {
memset(buff, 0, 1024);
printf("请输入要发送的信息:");
scanf("%s", buff);
//用clientSocket 套接字将buff中的内容发送给serverAddr中配置的udp端口
r = sendto(clientSocket, buff, strlen(buff), NULL, (sockaddr*)&serverAddr, sizeof serverAddr);
}
//8 关闭socket
closesocket(clientSocket);
//9 清除协议版本
WSACleanup();
while (1);
return 0;
}
0x03 TCP 的11种状态补充
三次握手:
四次挥手:
比如说:客户端要断开连接,先给服务器发送fin
服务器收到fin后回应一个ack,然后开始结束自己未结束的工作
残局收拾完后,服务器给客户端发送一个fin,客户端响应一个ack
断开连接
tcp端口的11种状态:
CLOSED:连接建立前和连接断开后的状态
LISTENING:监听状态,服务器端口已经准备好了,在等待客户端连接
SYN_RCVD: 服务器收到了SYN,但是还没有发送SYN ACK 的状态
该状态很短暂
SYN_SEND : 连接请求SYN发送后,收到ACK之前的状态
ESTABLISHED:数据传输通道建立完毕
CLOSE_WAIT: 客户端发送FIN后,接收到ACK前的状态
LAST_ACK: 客户端接收到FIN,但是还未发送ACK (这段时间客户端在回收自己的资源)
FIN_WAIT_1: 服务器向对方发送FIN后,接收ACK前
FIN_WAIT_2: 收到对方接到FIN后的ACK,等待对方发送FIN
CLOSING:双方同时发送FIN,同时收到ACK
TIME_WAIT:网络延迟等待状态