目录
一、基于TCP的服务端/客户端
套接字(Socket)是在网络通信中使用的一种抽象概念,它允许不同计算机之间的进程进行通信。套接字可以看作是网络通信的端点,通过套接字,进程可以在网络上发送和接收数据。
在实际编程中,套接字通常由操作系统提供的 API 来创建和操作。常见的套接字类型包括流式套接字(用于 TCP 协议)和数据报套接字(用于 UDP 协议)。套接字通常由以下几个要素确定:
- IP 地址:指定目标主机的地址,可以是 IPv4 或 IPv6 地址。
- 端口号:用于区分同一主机上不同套接字的通信端口。
- 协议:指定使用的通信协议,如 TCP 或 UDP。
套接字通常遵循客户端-服务器模型,其中一个套接字作为服务器监听连接请求,另一个套接字作为客户端发起连接请求。一旦连接建立,套接字之间就可以进行数据的收发。
SOCKADDR_IN 结构体是用于存储 socket 地址信息的数据结构,通常用于 IPv4 网络编程中。简单解释一下 SOCKADDR_IN 结构体及其用途:
-
存储地址信息: SOCKADDR_IN 结构体用于存储 IP 地址和端口号等网络地址信息,它包含了目标主机的地址和端口信息,以便在网络通信中被使用。
-
IPv4 地址表示: 在 SOCKADDR_IN 结构体中,IP 地址通常以一个名为 sin_addr 的 in_addr 类型字段表示,该字段存储了目标主机的 IPv4 地址,以便在网络通信中使用。
-
端口号表示: SOCKADDR_IN 结构体中的 sin_port 字段用于存储端口号,指定了在目标主机上监听或连接的端口。
-
地址族表示: SOCKADDR_IN 结构体中的 sin_family 字段用于指定地址族,通常为 AF_INET,表示 IPv4 地址族。
-
网络字节序转换: 在使用 SOCKADDR_IN 结构体时,通常需要通过一些函数(如 htons、htonl)来将主机字节序转换为网络字节序,以确保数据在网络中传输时的正确性。
总的来说,SOCKADDR_IN 结构体用于存储目标主机的地址和端口信息,是在网络编程中非常常用的数据结构。在创建套接字、绑定地址、发起连接等网络操作中,我们经常需要使用 SOCKADDR_IN 结构体来指定和操作网络地址信息。
服务端思路:
- 加载并初始化套接字库
- 创建TCP套接字 socket()
- 绑定套接字到本机 bind()
- 服务端开始监听 listen()
- 服务端接受请求通话 accept()
- 接收数据,发送数据
- 关闭套接字
- 清理套接字库
/*
*/
#include <WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
/*
在网络编程中,"ws2_32.lib" 是Windows套接字API的库文件,它包含了一系列函数和定义,用于实现套接字编程和网络通信。
该库提供了对套接字、协议、网络地址等相关操作的支持。
通过添加 #pragma comment(lib,"ws2_32.lib") 到源代码中,编译器会在编译过程中自动将 "ws2_32.lib" 这个库文件与生成的可执行文件进行链接,以便在运行时能够正确地调用网络编程所需的函数和定义。
*/
// 一个针对Windows平台的编译指令,用于告诉编译器链接到名为 "ws2_32.lib" 的库文件。
#pragma comment(lib,"ws2_32.lib")
int main()
{
printf("Server\n");
//1 初始化网络库
// 加载套接字库
// WORD = unsigned short
WORD wVersionRequested; // 用于指定请求的 Winsock 版本号。
// WSADATA 结构体
WSADATA wsaData; // 用于存储 Winsock 的详细信息,包括版本、实现和配置等。
int err; // 用于存储函数调用返回的错误代码。
/*
MAKEWORD(2, 2) 是一个宏,用于将两个字节大小的版本号组合成一个 WORD 类型的值。
这里将版本号设置为 2.2,表示请求使用 Winsock 2.2 版本。
*/
wVersionRequested = MAKEWORD(2, 2);
// 初始化套接字库
/*
int WSAAPI WSAStartup(_In_ WORD wVersionRequested, _Out_ LPWSADATA lpWSAData);
参数:1. 一个 WORD 类型的变量,指示请求的 Winsock 版本号。
2. 一个指向 WSADATA 结构体的指针,用于接收 Winsock 库的详细信息。
返回值:函数调用成功后,如果返回值为 0,表示 Winsock 库已成功初始化,并且可以继续使用 Winsock 函数进行套接字编程。
如果返回值不为 0,则表示初始化失败,需要根据返回值进行错误处理。
需要注意的是,在使用完 Winsock 库后,应该调用 WSACleanup 函数来释放 Winsock 资源。
*/
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
printf("WSAStartup errorNum = %d\n", GetLastError());
return err;
}
/*
通过 LOBYTE 和 HIBYTE 宏来检查 wsaData.wVersion 的值,以确保所请求的 Winsock 版本号为 2.2。具体来说:
LOBYTE(wsaData.wVersion) 用于获取 wsaData.wVersion 的低字节,即主版本号。
HIBYTE(wsaData.wVersion) 用于获取 wsaData.wVersion 的高字节,即副版本号。
*/
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("LOBYTE errorNum = %d\n", GetLastError());
// 调用 WSACleanup 函数释放 Winsock 资源。
WSACleanup();
return -1;
}
// 2 安装电话机
// 新建套接字
/*
SOCKET = unsigned int
调用 socket 函数来创建一个套接字,参数:
AF_INET:指定地址族为 IPv4 地址族,表示套接字将用于 Internet 地址。
SOCK_STREAM:指定套接字类型为流式套接字,用于 TCP 协议。
0:指定协议,通常为 0,表示根据指定的地址族和套接字类型自动选择合适的协议。
如果 socket 函数返回的套接字句柄等于 INVALID_SOCKET,则表示套接字创建失败
*/
SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockSrv)
{
printf("socket errorNum = %d\n", GetLastError());
return -1;
}
//给变量配置电话号码 IP 任何 端口6000
// 这段代码的作用是设置服务器地址为任意 IP 地址,地址族为 IPv4,端口号为 6000。
SOCKADDR_IN addrSrv; // 定义了一个名为 addrSrv 的 SOCKADDR_IN 结构体变量来存储服务器的地址信息。
// sin_addr:一个 in_addr 结构体类型的字段,用于指定 IP 地址。
// S_un.S_addr:一个 ULONG 类型的字段,用于存储 IP 地址的二进制表示。INADDR_ANY 是一个宏定义,表示任意地址。
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
// 一个 short 类型的字段,用于指定地址族。AF_INET 表示 IPv4 地址族。
addrSrv.sin_family = AF_INET;
// 一个 u_short 类型的字段,用于指定端口号。
// htons 函数用于将主机字节序转换为网络字节序,确保字节序的正确性。这里端口号设置为 6000。
addrSrv.sin_port = htons(6000);
// 3 分配电话号码
/*
绑定套接字到本地IP地址,端口号6000
int WSAAPI bind(
_In_ SOCKET s, 要绑定的套接字。
_In_reads_bytes_(namelen) const struct sockaddr FAR * name, 一个指向 SOCKADDR 结构体的指针
_In_ int namelen 要绑定的地址结构体的大小
);
返回值如果是 SOCKET_ERROR ,说明绑定失败
*/
if (SOCKET_ERROR == bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)))
{
printf("bind errorNum = %d\n", GetLastError());
return -1;
}
// 4、监听 listen
/*
在服务器端程序中调用 listen 函数,用于将套接字设置为监听状态,以便接受客户端的连接请求。
参数说明:
sockSrv 是一个套接字描述符,用于表示服务器端的套接字。
5 是作为参数传递给 listen 函数的第二个参数,它指定了服务器端允许同时连接的最大客户端数量
当服务器调用 listen 函数后,套接字就会进入监听状态,开始等待客户端的连接请求。
同时,操作系统会维护一个待处理连接请求的队列,长度由第二个参数决定。
如果有新的客户端连接请求到达,且待处理连接请求的队列未满,服务器将会接受这个连接请求并创建一个新的套接字与客户端进行通信。
*/
if (SOCKET_ERROR == listen(sockSrv, 5))
{
printf("listen errorNum = %d\n", GetLastError());
return -1;
}
// 5、拿起话筒,准备通话
/*
SOCKADDR 是一个通用的套接字地址结构体,它是在网络编程中经常使用的数据结构。
在 Windows 平台上,SOCKADDR 结构体通常是 SOCKADDR_IN 结构体的基类,用于表示通用的套接字地址。
在 Unix/Linux 平台上,通常使用 sockaddr 结构体来表示套接字地址。
总的来说,SOCKADDR 提供了一个通用的接口,用于在网络编程中处理和传递套接字地址信息,它是实现网络通信的重要组成部分。
*/
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR);
while (TRUE)
{
//6、分配一台分机去服务
/*
当服务器调用 accept 函数后,它会阻塞在这个函数上,直到有客户端发起连接请求。一旦有连接请求到达,accept 函数会创建一个新的套接字,返回一个新的套接字描述符 sockConn。这个新的套接字可以用于与客户端进行通信。
同时,accept 函数会将客户端的地址信息填充到 addrCli 指向的内存空间中,并将实际的地址长度更新到 len 指向的变量中。
SOCKET WSAAPI accept(
_In_ SOCKET s, 服务器端的监听套接字,用于接受客户端的连接请求
_Out_writes_bytes_opt_(*addrlen) struct sockaddr FAR * addr, 客户端的地址信息填充到 addrCli 指向的内存空间中
_Inout_opt_ int FAR * addrlen 将实际的地址长度更新到 len 指向的变量中。
);
*/
SOCKET sockConn = accept(sockSrv, (SOCKADDR*)&addrCli, &len);
char sendBuf[100] = { 0 };
// 是一个用于将 IPv4 的网络地址转换为字符串形式的函数,它通常在网络编程中使用。
sprintf_s(sendBuf, 100, "Welcome %s to bingo!", inet_ntoa(addrCli.sin_addr));
//发送数据
/*
当调用 send 函数后,它会将 sendBuf 中的数据发送到 sockConn 所代表的套接字连接中。
函数返回值 iLen 表示实际发送的字节数,如果发生错误则返回一个特定的错误代码。
int WSAAPI send(
_In_ SOCKET s, 一个已经建立的套接字连接,用于与另一端进行通信
_In_reads_bytes_(len) const char FAR * buf, 要发送数据的缓冲区
_In_ int len, strlen(sendBuf) + 1 表示要发送的数据的长度,加1是为了包括字符串结束符'\0'。
_In_ int flags 是一个标志参数,用于指定发送操作的行为。
);
*/
int iLen = send(sockConn, sendBuf, strlen(sendBuf) + 1, 0);
if (iLen < 0)
{
printf("send errorNum = %d\n", GetLastError());
return -1;
}
char recvBuf[100] = { 0 };
//接收数据
/*
当调用 recv 函数后,它会尝试从 sockConn 所代表的套接字连接中接收数据,并将数据存储到 recvBuf 中。
函数返回值 iLen 表示实际接收的字节数,如果返回 0 表示对方已经关闭连接,如果返回 -1 则表示接收时发生了错误。
int WSAAPI recv(
_In_ SOCKET s, 一个已经建立的套接字连接,用于与另一端进行通信。
_Out_writes_bytes_to_(len, return) __out_data_source(NETWORK) char FAR * buf, 用于接收数据的缓冲区
_In_ int len, 最多可以接收的字节数
_In_ int flags 是一个标志参数,用于指定接收操作的行为。
);
*/
iLen = recv(sockConn, recvBuf, 100, 0);
if (iLen < 0)
{
printf("recv errorNum = %d\n", GetLastError());
return -1;
}
//打印接收的数据
printf("recvBuf = %s\n", recvBuf);
closesocket(sockConn); // 将关闭这个套接字连接,释放相关的资源。
}
//7 关闭总机
closesocket(sockSrv);
WSACleanup();
system("pause");
return 0;
}
客户端思路:
- 加载并初始化套接字库
- 创建TCP套接字 socket()
- 连接服务器 connect()
- 接收发送数据
- 关闭套接字
- 清理套接字库
#include <WinSock2.h>
#include <stdio.h>
#include <stdlib.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
printf("Client\n");
char sendBuf[] = "hello,world";
//1 初始化网络库
// 加载套接字库
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
// 1、初始化套接字库
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
printf("WSAStartup errorNum = %d\n", GetLastError());
return err;
}
if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2)
{
printf("LOBYTE errorNum = %d\n", GetLastError());
WSACleanup();
return -1;
}
// 2 安装电话机
// 新建套接字
SOCKET sockCli = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == sockCli)
{
printf("socket errorNum = %d\n", GetLastError());
return -1;
}
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("192.168.8.253");
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6000);
// 3 连接服务器
if (SOCKET_ERROR == connect(sockCli, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)))
{
printf("connect errorNum = %d\n", GetLastError());
return -1;
}
// 4 接收和发送数据
char recvBuf[100] = { 0 };
int iLen = recv(sockCli, recvBuf, 100, 0);
if (iLen < 0)
{
printf("recv errorNum = %d\n", GetLastError());
return -1;
}
printf("Client recvBuf = %s\n", recvBuf);
// 发送数据
iLen = send(sockCli, sendBuf, strlen(sendBuf) + 1, 0);
if (iLen < 0)
{
printf("send errorNum = %d\n", GetLastError());
return -1;
}
// 关闭套接字
closesocket(sockCli);
WSACleanup();
system("pause");
return 0;
}
二、VS2019关闭 4996
- 打开项目的“属性页”对话框。 有关如何使用“属性页”对话框的信息,请参阅属性页。
- 选择“配置属性”>“C/C++”>“高级”属性页。
- 编辑“禁用特定警告”属性以添加 4996 。 选择“确定”以应用更改
三、基于UDP的服务端/客户端
服务端:
- 创建并初始化套接字库
- 创建socket
- 绑定到本机
- 接收发送数据
- 关闭套接字库
- 关闭套接字
#include <WinSock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main()
{
// 初始化套接字库
WORD wVersion;
WSADATA wsaData;
int err;
wVersion = MAKEWORD(1, 1);
err = WSAStartup(wVersion, &wsaData);
if (err != 0)
{
return err;
}
if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup();
return -1;
}
// 创建套接字
SOCKET sockSrv = socket(AF_INET, SOCK_DGRAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(6001);
// 绑定套接字
bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
// 等待并接收数据
SOCKADDR_IN addrCli;
int len = sizeof(SOCKADDR_IN);
char recvBuf[100];
char sendBuf[100];
while (true)
{
recvfrom(sockSrv, recvBuf, 100, 0, (SOCKADDR*)&addrCli, &len);
std::cout << recvBuf << std::endl;
sprintf_s(sendBuf, 100, "Ack %s", recvBuf);
sendto(sockSrv, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrCli, len);
}
closesocket(sockSrv);
WSACleanup();
system("pause");
return 0;
}
客户端:
- 创建并初始化套接字库
- 创建socket
- 发送数据
#include <WinSock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
int main()
{
// 加载套接字库
WORD wVersion;
WSADATA wsaData;
int err;
wVersion = MAKEWORD(1, 1);
err = WSAStartup(wVersion, &wsaData);
if (err != 0)
{
return err;
}
if (LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{
WSACleanup();
return -1;
}
// 创建套接字
SOCKET sockCli = socket(AF_INET, SOCK_DGRAM, 0);
SOCKADDR_IN addrSrv;
addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
addrSrv.sin_port = htons(6001);
addrSrv.sin_family = AF_INET;
int len = sizeof(SOCKADDR);
char sendBuf[] = "hello";
char recvBuf[100];
//发送数据
sendto(sockCli, sendBuf, strlen(sendBuf) + 1, 0, (SOCKADDR*)&addrSrv, len);
recvfrom(sockCli, recvBuf, 100, 0, (SOCKADDR*)&addrSrv, &len);
std::cout << recvBuf << std::endl;
closesocket(sockCli);
system("pause");
return 0;
}