【第21节】windows sdk编程:网络编程基础
目录
引言:网络编程基础
一、socket介绍(套接字)
1.1 Berkeley Socket套接字
1.2 WinSocket套接字
1.3 WSAtartup函数
1.4 socket函数
1.5 字节序转换
1.6 绑定套接字
1.7 监听
1.8 连接
1.9 接收数据
1.10 发送数据
1.11 关闭套接字
二、UDP连接流程
2.1 接收数据
2.2 发送数据
三、阻塞与非阻塞模式
四、示例代码
4.1 TCP协议代码
4.2 UDP协议代码
引言:网络编程基础
在网络编程领域,实现高效、可靠的网络通信至关重要。套接字(Socket)作为网络通信的关键接口,在其中扮演着核心角色。从最初加利福尼亚大学Berkeley分校为UNIX系统开发的Berkeley Socket,到后来多家公司共同制定的Windows Sockets规范,套接字不断发展完善。了解套接字的原理、相关函数的使用以及不同网络协议(如TCP、UDP)的连接流程,对于开发稳定的网络应用程序意义重大。
一、socket介绍(套接字)
1.1 Berkeley Socket套接字
套接字(Socket)最初是由加利福尼亚大学Berkeley分校专门为UNIX操作系统搞出来的网络通信接口。时间回到20世纪80年代初,这所学校把美国国防部高研署提供的TCP/IP整合进了Unix系统里,紧接着,很快就开发出了TCP/IP应用程序接口(API),这个接口其实就是Socket(套接字)接口。后来,UNIX操作系统用的人越来越多,套接字也跟着火了起来,成了现在最常用的网络通信应用程序接口之一。
1.2 WinSocket套接字
在90年代初期,Sun Microsystems、JSB Corporation、FTP software、Microdyne还有Microsoft这几家公司一起搞出了一套标准,叫做Windows Sockets规范。这个规范是对Berkeley Sockets的一个重要升级。具体来说,它新添了一些异步函数,还弄出了符合Windows消息驱动特点的网络事件异步选择机制。
Windows Sockets规范是一套开放的网络编程接口,能支持多种协议,专门用于Windows系统。在实际使用中,Windows Sockets规范主要有1.1版和2.0版这两个版本。1.1版只能支持TCP/IP协议,而2.0版就厉害了,它能支持好几种协议,并且对于之前的版本,也能很好地兼容,老程序也能正常使用。
TCP连接流程
1. 包含必要的头文件及库:
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
2. 指定需要使用的Winsock规范的最高版本,并初始化Winsock,装入Winsock.dll:
WSAStartup(MAKEWORD(2,2),&wsaDATA);
3. 创建套接字:
socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
4. 绑定IP和端口:
bind(sock,(sockaddr*)&addr,sizeof(sockaddr_in));
5. 监听:
listen(sock,SOMAXCONN);
6. 连接客户端:
accept(sock,(sockaddr*)&addrClient,&nAddrSize);
7. 接收数据:
recv(sockClient,buf,1024,0);
8. 发送数据:
send(Sock,message,nSize,O);
9. 在调用“closesocket”函数之后,但是在程序结束之前需要清理Winsock:
closesocket(sock); //关闭套接字
WSACleanup();
1.3 WSAtartup函数
不管是开发客户端还是服务端的Socket应用程序,都必须先加载Windows Sockets动态库,通常使用WSAtartup函数来实现这个功能。
int WSAStartup(
WORD wVersionRequested,
LPWSADATA lpWSAData
);
- `wVersionRequested`:通过`MAKEWORD(X,Y)`宏定义来表示,`X`(高位)表示次版本号,`Y`(低位)表示主版本号,即期望调用者使用的WinSocket版本。
- `lpWSAData`:指向`WSADATA`结构体,用于返回被加载动态库的有关信息。
typedef struct WSAData {
WORD wVersion; //期望调用者使用的WinSocket版本
WORD wHighVersion;//DLL支持的最高版本
char szDescription[WSADESCRIPTION_LEN + 1];//DLL的描述信息
char szSystemStatus[WSASYS_STATUS_LEN + 1];//DLL的状态信息
unsigned short iMaxSockets;//一个进程可以打开套接字最多数量
unsigned short iMaxUdpDg;
char FAR* lpVendorInfo;//一个进程发送或接收的最大数据的长度
}WSADATA,*LPWSADATA;
1.4 socket函数
初始化WinSocket DLL后,通过`scoket`函数和`WSASocket`函数来创建套接字。该函数调用成功后,会返回一个新建的套接字句柄。
SOCKET WSAAPI socket(
_In_ int af, //通信协议族
_In_ int type, //套接字类型
_In_ int protocol);//传输协议
af:
- `AF_INET`:internet协议(IP V4)
- `AF_IRDA`:红外协议
- `AF_BTH`:蓝牙协议
type:
- `SOCK_STREAM`:流式socket(需建立连接,通信过程可靠TCP)
- `SOCK_DGRAM`:数据报socket(无需建立连接,通讯过程不可靠UDP)
- `SOCK_RAW`:原始套接字
Protocol:
- 对于`SOCK_STREAM`套接字类型,该字段为`IPPROTO_TCP`或者`0`。
- 对于`SOCK_DGRAM`套接字类型,该字段为`IPPROTO_UDP`或者`0`。
(1)流套接字
流套接字能够提供双向的数据流服务,数据传输是有序的,不会重复,也不存在记录边界,特别适合处理大量数据的情况。在网络传输层,它可以根据需要把数据分散成合适大小的数据包,或者把数据包集中起来。
使用流套接字通信时,双方得先建立一条通路。这么做一方面能确定双方之间的传输路线,另一方面能确保双方都处于活动状态,随时可以互相响应。不过,建立这样一个通信信道可不容易,要耗费不少资源。另外,大多数面向连接的协议,为了保证数据发送准确无误,往往得做一些额外的计算来验证数据的正确性,这又进一步增加了开销 。
(2)数据报套接字
数据报套接字能实现双向的数据流动。但它有个问题,没办法确保数据在传输过程中是可靠的,也不能保证数据按顺序到达,还可能出现重复数据。打个比方,一个进程通过数据报套接字接收信息时,可能会发现收到的信息跟发送时的顺序不一样,甚至还会收到重复的内容。
数据报套接字在工作时不需要建立连接,发送端发送信息后,它不管接收端是不是在监听,也不关心接收端有没有按时收到信息。正因为这样,数据报的可靠性比较差。所以在使用数据报套接字编程时,程序员得自己想办法去管理数据报的顺序,还要确保数据的可靠性 。
1.5 字节序转换
不同的计算机有时使用不同的字节顺序存储数据。任何从Winsock函数对IP地址和端口号的引用,以及传送给Winsock函数的IP地址和端口都是按照网络顺序组织的。
- 将32位数从网络字节转换成主机字节(大端到小端):
u_long ntohl(u_long hostlong);
- 将16位数从网络字节转换成主机字节(大端到小端):
u_short ntohs(u_short short);
- 将32位数从主机字节转换成网络字节(小端到大端):
u_long htonl(u_long hostlong);
- 将16位数从主机字节转换成网络字节(小端到大端):
U_short htons(u short short);
1.6 绑定套接字
`bind()`函数将套接字绑定到一个已知的地址上。
int bind(
SOCKET s, //套接字
struct sockaddr FAR*name,//地址结构体变量(IP,端口,协议簇
int namelen //Sockaddr结构长度
);
示例:
sockaddr_in addr;
addr.sin_family = AF_INET; //地址家族
addr.sin_port = htons(1234); //端口号
addr.sin_addr.S_un.S_addr = inet_addr("192.168.1.100"); //IP地址 本地:127.0.0.1
//3.绑定套接字
nErrCode = bind(sock,
(sockaddr*)&addr, //套接字
sizeof(sockaddr_in));//IP定址结构体大小
1.7 监听
`listen()`函数将套接字设置为监听模式。
1.8 连接
`accept`函数实现接收一个连接请求。
SOCKET accept(
SOCKET s, //监听套接字
struct sockaddr FAR*addr,
int FAR*addrlen
);
该函数返回请求连接的套接字句柄。
示例:
SOCKET ClientSocket = accept(sock,
(sockaddr*)&addrClient, //返回请求连接主机的地址
&nAddrSize); //sockaddr_in的大小
示例2:域名解析
hostent *phostent;
in_addr in; //指向hostent结构的指针 //IPV4地址结构
if((phostent = gethostbyname("www.15pb.com"))==NULL){
printf("gethostbyname()错误:%d",WSAGetLastError());
}
else{
//拷贝4字节的IP地址到IPV4地址结构
memcpy(&in,phostent->h_addr,4);
printf("主机%s的IP地址是:",phostent->h_name);
printf("%s",inet_ntoa(in));
1.9 接收数据
`Recv()`函数用于接收数据。
int recv(
SOCKET s,
char *buf, //接收数据缓冲区
int len,
int flags
);
该函数返回接收到的数据实际长度,最后一个参数可以是`0`、`MSG_PEEK`和`MSG_OOB`。
- `0`表示无特殊行为。
- `MSG_PEEK`表示会使有用的数据被复制到接收缓冲区,但没有从系统中将其删除。
- `MSG_OOB`表示处理带外数据。
示例:
char buf[1024]={0};
nRecvSize = recv(sockClient, buf, 1024, 0);
1.10 发送数据
`send()`函数用于发送数据。
int send(
SOCKET s,
char *buf, //发送数据缓冲区
int len,
int flags
);
示例:
send(Sock, message, nSize, 0);
1.11 关闭套接字
`Closecocket()`函数关闭套接字,释放所占资源。
closecocket(SOCKET s //要关闭的套接字);
当调用该函数释放套接字后,如果再使用该套接字执行函数调用,则会失败并返回`WSAENOTSOCK`错误。
二、UDP连接流程
1. 包含必要的头文件及库:
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")
2. 指定需要使用的Winsock规范的最高版本,并初始化Winsock,装入Winsock.dll:
WSAStartup(MAKEWORD(2,2),&wsaDATA);
3. 创建套接字:
socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
4. 绑定IP和端口:
bind(sock,(sockaddr*)&addr,sizeof(sockaddr_in));
5. 接收数据:
recvfrom(sockClient,buf,1024,0,(sockaddr*)&fromAddr,&fromLen);
6. 发送数据:
sendto(Sock,message,nSize,0,(sockaddr*)&toAddr,toLen);
7. 在调用“closesocket”函数之后,但是在程序结束之前需要清理Winsock:
closesocket(sock); //关闭套接字
WSACleanup();
2.1 接收数据
`recvfrom()`函数用于接收数据,并且返回发送数据主机的地址。
recvfrom(
SOCKET s, //用来接收数据的套接字
char FAR*buf, //接收数据的缓冲区
int len, //接收缓冲区的长度
int flags, //一般为0
struct sockaddr*to, //接收的地址结构
int FAR*fromlen //sockaddr结构大小
);
注:函数的返回值,是接收到的大小。
2.2 发送数据
`sendto()`函数用于发送数据。
sendto(
SOCKET s,
const char FAR*buf,
int len,
int flags,
const struct sockaddr*to,
int tolen
);
- `s`:用来发送数据的套接字。
- `buf`:发送数据的缓冲区。
- `len`:要发送数据的长度。
- `flags`:一般为`0`。
- `to`:目标地址和端口。
- `tolen`:`sockaddr`结构大小。
如果该函数成功则返回发送数据的字节数。
需要注意以下两点:
1. 在UDP编程里,从程序编写的角度来看,很难明确区分出服务端和客户端。简单来说,谁提供服务,就把谁当作服务端。
2. 还有一点要留意,当刚创建好socket时,直接调用sendto函数是可行的,此时不需要手动绑定,系统会自动进行绑定操作。
三、阻塞与非阻塞模式
阻塞模式
在阻塞模式中,一旦执行操作函数,它就会一直处于等待状态,不会马上给出返回结果。执行这个函数的线程也会卡在这儿,只有当特定条件满足了,函数才会返回。打个比方,就像快递员通知你今天会有个快递送达,却没告诉你具体时间。在阻塞模式下,你就只能一直在校门口干等着快递,这一整天别的事都做不了。
非阻塞模式
要是处于非阻塞模式,操作函数执行后会立刻返回,执行这个函数的线程能接着往下运行。同样拿快递的例子来说,快递员告知你今天有快递,没说具体时间,你不用一直守在校门口,而是每隔30分钟去校门口瞅瞅快递到了没,要是没到就回来继续做自己原来的事。
为了避免线程长时间被阻塞,提升线程的使用效率,就出现了非阻塞模型。我们可以通过调用`ioctlsocket()`函数,来让socket明确处于非阻塞模式 。
int ioctlsocket(
_In_ SOCKET s,
_In_ long cmd,
_Inout_ u_long *argp
);
示例:
//2.设置套接字非阻塞模式
unsigned long ul = 1; //设置套接字选项
int nRet = ioctlsocket(sSocket,FIONBIO,&ul);
四、示例代码
4.1 TCP协议代码
服务端:
#include <winsock2.h> // Winsock库头文件
#include <ws2tcpip.h> // 提供IP地址转换函数
#include <iostream> // 标准输入输出流
#pragma comment(lib, "Ws2_32.lib") // 链接Winsock库
// 初始化Winsock库
bool InitWinSock() {
WSADATA wsaData; // 用于存储Winsock库的初始化信息
// 调用WSAStartup初始化Winsock库,版本2.2
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed!" << std::endl;
return false; // 初始化失败返回false
}
return true; // 初始化成功返回true
}
// 创建并绑定套接字
SOCKET CreateAndBindSocket(const char* ip, int port) {
// 创建TCP套接字
SOCKET sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sock == INVALID_SOCKET) {
std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;
return INVALID_SOCKET; // 创建套接字失败返回INVALID_SOCKET
}
sockaddr_in addr{}; // 定义一个IPv4地址结构体
addr.sin_family = AF_INET; // 地址族为IPv4
addr.sin_port = htons(port); // 设置端口号,转换为网络字节序
// 将IP地址从字符串转换为二进制格式
inet_pton(AF_INET, ip, &addr.sin_addr);
// 绑定套接字到指定IP和端口
if (bind(sock, (sockaddr*)&addr, sizeof(addr)) == SOCKET_ERROR) {
std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;
closesocket(sock); // 绑定失败关闭套接字
return INVALID_SOCKET; // 返回INVALID_SOCKET
}
return sock; // 返回绑定成功的套接字
}
// 主函数
int main() {
// 初始化Winsock库
if (!InitWinSock()) return 1;
const char* serverIp = "192.168.1.100"; // 服务器IP地址
int serverPort = 1234; // 服务器端口号
// 创建并绑定套接字
SOCKET serverSocket = CreateAndBindSocket(serverIp, serverPort);
if (serverSocket == INVALID_SOCKET) {
WSACleanup(); // 释放Winsock资源
return 1;
}
// 开始监听连接请求,SOMAXCONN为最大连接数
if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) {
std::cerr << "Listen failed: " << WSAGetLastError() << std::endl;
closesocket(serverSocket); // 监听失败关闭套接字
WSACleanup(); // 释放Winsock资源
return 1;
}
std::cout << "Server listening on " << serverIp << ":" << serverPort << std::endl;
// 主循环,处理客户端连接
while (true) {
sockaddr_in clientAddr{}; // 存储客户端地址信息
int clientAddrSize = sizeof(clientAddr);
// 接受客户端连接,返回客户端套接字
SOCKET clientSocket = accept(serverSocket, (sockaddr*)&clientAddr, &clientAddrSize);
if (clientSocket == INVALID_SOCKET) {
std::cerr << "Accept failed: " << WSAGetLastError() << std::endl;
continue; // 接受失败继续循环
}
char clientIp[INET_ADDRSTRLEN]; // 存储客户端IP地址字符串
// 将客户端IP地址从二进制转换为字符串格式
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN);
std::cout << "Client connected: " << clientIp << std::endl;
char buffer[1024]; // 接收数据的缓冲区
while (true) {
// 接收客户端发送的数据
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived <= 0) {
std::cerr << "Client disconnected or recv failed: " << WSAGetLastError() << std::endl;
break; // 接收失败或客户端断开连接,退出循环
}
buffer[bytesReceived] = '\0'; // 确保字符串以NULL结尾
std::cout << "Received: " << buffer << std::endl; // 打印接收到的数据
}
closesocket(clientSocket); // 关闭客户端套接字
}
closesocket(serverSocket); // 关闭服务器套接字
WSACleanup(); // 释放Winsock资源
return 0;
}
客户端:
#include <winsock2.h> // Winsock库头文件
#include <ws2tcpip.h> // 提供IP地址转换函数
#include <iostream> // 标准输入输出流
#pragma comment(lib, "Ws2_32.lib") // 链接Winsock库
// 初始化Winsock库
bool InitWinSock() {
WSADATA wsaData; // 用于存储Winsock库的初始化信息
// 调用WSAStartup初始化Winsock库,版本2.2
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "WSAStartup failed!" << std::endl;
return false; // 初始化失败返回false
}
return true; // 初始化成功返回true
}
// 主函数
int main() {
// 初始化Winsock库
if (!InitWinSock()) return 1;
const char* serverIp = "127.0.0.1"; // 服务器IP地址
int serverPort = 1234; // 服务器端口号
// 创建TCP套接字
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET) {
std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;
WSACleanup(); // 释放Winsock资源
return 1;
}
sockaddr_in serverAddr{}; // 定义一个IPv4地址结构体
serverAddr.sin_family = AF_INET; // 地址族为IPv4
serverAddr.sin_port = htons(serverPort); // 设置端口号,转换为网络字节序
// 将IP地址从字符串转换为二进制格式
inet_pton(AF_INET, serverIp, &serverAddr.sin_addr);
// 连接到服务器
if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cerr << "Connection failed: " << WSAGetLastError() << std::endl;
closesocket(clientSocket); // 连接失败关闭套接字
WSACleanup(); // 释放Winsock资源
return 1;
}
std::cout << "Connected to server at " << serverIp << ":" << serverPort << std::endl;
char message[1024]; // 存储发送的消息
std::cout << "Enter message: ";
std::cin.getline(message, sizeof(message)); // 从标准输入读取消息
// 发送消息到服务器
if (send(clientSocket, message, static_cast<int>(strlen(message)), 0) == SOCKET_ERROR) {
std::cerr << "Send failed: " << WSAGetLastError() << std::endl;
}
closesocket(clientSocket); // 关闭客户端套接字
WSACleanup(); // 释放Winsock资源
return 0;
}
4.2 UDP协议代码
#include <winsock2.h> // Winsock库头文件
#include <ws2tcpip.h> // 提供IP地址转换函数
#include <iostream> // 标准输入输出流
#pragma comment(lib, "Ws2_32.lib") // 链接Winsock库
// 函数名称:InitWinSock
// 功能:初始化Winsock库
// 返回值:成功返回TRUE,失败返回FALSE
BOOL InitWinSock() {
WSADATA wsaData; // 用于存储Winsock库的初始化信息
// 调用WSAStartup初始化Winsock库,版本2.2
int nResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (nResult != 0) { // 初始化失败
std::cerr << "WSAStartup failed with error: " << nResult << std::endl;
return FALSE;
}
return TRUE; // 初始化成功
}
// 函数名称:InitServer
// 功能:初始化UDP服务器
// 返回值:成功返回true,失败返回false
bool InitServer() {
// 1. 初始化Winsock库
if (!InitWinSock()) {
return false;
}
// 2. 创建UDP套接字
SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serverSocket == INVALID_SOCKET) {
std::cerr << "Socket creation failed: " << WSAGetLastError() << std::endl;
WSACleanup(); // 释放Winsock资源
return false;
}
// 3. 定义服务器地址
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET; // 地址族为IPv4
serverAddr.sin_port = htons(1234); // 设置端口号,转换为网络字节序
// 将IP地址从字符串转换为二进制格式
inet_pton(AF_INET, "192.168.199.207", &serverAddr.sin_addr);
// 4. 绑定套接字到指定IP和端口
if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
std::cerr << "Bind failed: " << WSAGetLastError() << std::endl;
closesocket(serverSocket); // 绑定失败关闭套接字
WSACleanup(); // 释放Winsock资源
return false;
}
std::cout << "Server started and listening on 192.168.199.207:1234" << std::endl;
// 5. 主循环,接收客户端数据
while (true) {
char buffer[1024] = {0}; // 接收数据的缓冲区
sockaddr_in clientAddr{}; // 存储客户端地址信息
int clientAddrSize = sizeof(clientAddr);
// 接收客户端发送的数据
int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0,
(sockaddr*)&clientAddr, &clientAddrSize);
if (bytesReceived == SOCKET_ERROR) {
std::cerr << "recvfrom failed: " << WSAGetLastError() << std::endl;
break; // 接收失败退出循环
}
// 打印接收到的数据
buffer[bytesReceived] = '\0'; // 确保字符串以NULL结尾
char clientIp[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN); // 将客户端IP转换为字符串
std::cout << "Received from " << clientIp << ": " << buffer << std::endl;
}
// 6. 关闭套接字并释放资源
closesocket(serverSocket);
WSACleanup();
return true;
}
// 主函数
int main() {
if (!InitServer()) {
std::cerr << "Server initialization failed!" << std::endl;
return 1;
}
return 0;
}
这篇网络编程基础的内容全面介绍了套接字相关知识,涵盖了Berkeley Socket和WinSocket的起源与发展,详细阐述了TCP和UDP的连接流程,包括各个步骤中涉及的函数使用方法、参数含义,还介绍了阻塞与非阻塞模式的概念及设置方式,并通过完整的TCP和UDP协议示例代码,让读者能够更直观地理解和实践网络编程中的关键操作。