一、开发环境
我这里介绍下我用的环境安装过程。 所有版本的VS都可以的。
我当前环境是在Windows下,IDE用的是地表最强IDE VS2022。
下载地址:https://visualstudio.microsoft.com/zh-hans/downloads/
因为我这里只需要用到C++和C语言编程,那么安装的时候可以自己选择需要安装的包。
安装好之后,创建项目。
二、网络编程的基础知识
2.1 什么是网络编程
网络编程是通过使用IP地址和端口号等网络信息,使两台以上的计算机能够相互通信,按照规定的协议交换数据的编程方式。
在网络编程中,程序员使用各种协议和技术,使得不同的设备可以通过网络进行数据交换和信息共享。
要实现网络编程,程序员需要了解并掌握各种网络通信协议,比如TCP/IP协议族,包括TCP、UDP、IP等,这些协议是实现设备间通信的基础。网络编程内部涉及到数据的打包、组装、发送、接收、解析等一系列过程,以实现信息的正确传输。
在TCP/IP协议族中,TCP和UDP是位于IP协议之上的传输层协议。 在OSI模型中,传输层是第四层,负责总体数据传输和数据控制,为会话层等高三层提供可靠的传输服务,为网络层提供可靠的目的地点信息。在TCP/IP协议族中,TCP和UDP正是位于这一层的协议。
这篇文章主要介绍 TCP 和 UDP 协议 以及 使用方法。
2.2 TCP 和 UDP协议介绍
TCP协议:
TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。在传输数据之前需要先建立连接,确保数据的顺序和完整性。TCP通过三次握手建立连接,并通过确认、超时和重传机制确保数据的可靠传输。TCP采用流量控制和拥塞控制机制,以避免网络拥塞,确保数据的顺利传输。因为TCP的这些特性,通常被应用于需要高可靠性和顺序性的应用,如网页浏览、电子邮件等。
UDP协议:
UDP(用户数据报协议)是一种无连接的、不可靠的传输层协议。与TCP不同,UDP在传输数据之前不需要建立连接,直接将数据打包成数据报并发送出去。因此,UDP没有TCP的那些确认、超时和重传机制,也就不保证数据的可靠传输。UDP也没有TCP的流量控制和拥塞控制机制。因为UDP的简单性和高效性,通常被应用于实时性要求较高,但对数据可靠性要求不高的应用,如语音通话、视频直播等。
2.3 TCP通信的实现过程
要实现TCP通信,两端必须要知道对方的IP和端口号:
(1)IP地址:TCP协议是基于IP协议进行通信的,因此需要知道对方的IP地址,才能建立连接。
(2)端口号:每个TCP连接都有一个唯一的端口号,用于标识进程和应用程序。建立连接时,需要指定本地端口号和远端端口号。
(3)应用层协议:TCP协议只提供数据传输服务,应用程序需要定义自己的应用层协议,用于解析报文和处理数据。例如,HTTP协议就是基于TCP协议的应用层协议。
在正常的TCP通信过程中,第一步需要建立连接,这个过程称为“三次握手”。建立连接时,客户端向服务器发送一个SYN包,表示请求建立连接;服务器接收到SYN包后,向客户端发送一个ACK包,表示确认收到了SYN包;最后客户端再向服务器发送一个ACK包,表示确认收到了服务器的ACK包,此时连接建立成功。建立连接后,数据传输就可以开始了。
三、Windows下的API介绍
微软的官方文档地址:https://learn.microsoft.com/zh-cn/windows/win32/api/_winsock/
3.1 常用的函数介绍
在Windows下进行网络编程,可以使用Winsock API(Windows Sockets API)来实现。Winsock API是Windows平台上的标准网络编程接口,提供了一系列函数和数据结构,用于创建、连接、发送和接收网络数据等操作。
下面是常用的Winsock API接口函数:
(1)
(2)
(3)
(4)
(5)
(6)
(7)
(8)
(9)
(10)
(11)
(12)
(13)
(14)
3.2 函数参数介绍
下面是常用的几个Winsock API函数及其函数原型和参数含义的介绍:
(1)
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
wVersionRequested :请求的Winsock版本号。lpWSAData :指向WSADATA结构的指针,用于接收初始化结果和相关信息。
(2)
SOCKET socket(int af, int type, int protocol);
af :地址族(Address Family),如AF_INET表示IPv4。type :套接字类型,如SOCK_STREAM表示面向连接的TCP套接字。protocol :指定协议。通常为0,表示根据type 自动选择合适的协议。
(3)
int bind(SOCKET s, const struct sockaddr* name, int namelen);
s :要绑定的套接字。name :指向sockaddr结构的指针,包含要绑定的本地地址信息。namelen :name 结构的长度。
(4)
int listen(SOCKET s, int backlog);
s :要监听的套接字。backlog :等待连接队列的最大长度。
(5)
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
s :监听套接字。addr :用于存储客户端地址信息的sockaddr结构。addrlen :addr 结构的长度。
(6)
int connect(SOCKET s, const struct sockaddr* name, int namelen);
s :要连接的套接字。name :指向目标地址信息的sockaddr结构指针。namelen :name 结构的长度。
(7)
int send(SOCKET s, const char* buf, int len, int flags);
s :要发送数据的套接字。buf :要发送的数据缓冲区。len :要发送的数据长度。flags :额外选项,如MSG_DONTROUTE等。
(8)
int recv(SOCKET s, char* buf, int len, int flags);
s :要接收数据的套接字。buf :用于存储接收数据的缓冲区。len :要接收的数据长度。flags :额外选项。
(9)
int sendto(SOCKET s, const char* buf, int len, int flags, const struct sockaddr* to, int tolen);
s :要发送数据的套接字。buf :要发送的数据缓冲区。len :要发送的数据长度。flags :额外选项。to :指向目标地址信息的sockaddr结构指针。tolen :to 结构的长度。
(10)
int recvfrom(SOCKET s, char* buf, int len, int flags, struct sockaddr* from, int* fromlen);
s :要接收数据的套接字。buf :用于存储接收数据的缓冲区。len :要接收的数据长度。flags :额外选项。from :用于存储发送方地址信息的sockaddr结构指针。fromlen :from 结构的长度。
(11)
int closesocket(SOCKET s);
s :要关闭的套接字。
(12)
int getaddrinfo(const char* nodename, const char* servname, const struct addrinfo* hints, struct addrinfo** res);
nodename :目标主机名或IP地址。servname :服务名或端口号。hints :指向addrinfo结构的指针,提供关于地址查找的提示。res :指向addrinfo结构链表的指针,用于接收查找结果。
(13)
struct hostent* gethostbyname(const char* name);
name :要查询的主机名。
(14)
int gethostname(char* name, int namelen);
name :用于接收主机名的缓冲区。namelen :name 缓冲区的长度。
四、基本示例代码
4.1 创建TCP服务器
下面代码实现一个简单的TCP服务器。
实现的功能:初始化Winsock、创建套接字、绑定到本地地址和指定端口、监听连接请求、接受客户端连接、发送和接收数据,最后关闭套接字和清理Winsock资源。
#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") // 链接到ws2_32库 int main() { WSADATA wsaData; int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化Winsock if (result != 0) { std::cout << "初始化Winsock失败 " << result << std::endl; return 1; } SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建套接字 if (listenSocket == INVALID_SOCKET) { std::cout << "创建套接字失败: " << WSAGetLastError() << std::endl; WSACleanup(); return 1; } sockaddr_in service; service.sin_family = AF_INET; service.sin_addr.s_addr = INADDR_ANY; service.sin_port = htons(12345); result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service)); // 将套接字绑定到本地地址和指定端口 if (result == SOCKET_ERROR) { std::cout << "端口绑定失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } result = listen(listenSocket, SOMAXCONN); // 监听连接请求 if (result == SOCKET_ERROR) { std::cout << "监听连接请求失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "等待客户端连接:" << std::endl; SOCKET clientSocket = accept(listenSocket, NULL, NULL); // 接受客户端连接 if (clientSocket == INVALID_SOCKET) { std::cout << "accept执行失败: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "客户端已连接..." << std::endl; char sendBuffer[1024] = "Hello, client!"; result = send(clientSocket, sendBuffer, sizeof(sendBuffer), 0); // 发送数据给客户端 if (result == SOCKET_ERROR) { std::cout << "发送消息执行错误: " << WSAGetLastError() << std::endl; closesocket(clientSocket); WSACleanup(); return 1; } char recvBuffer[1024]; result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0); // 接收来自客户端的数据 if (result == SOCKET_ERROR) { std::cout << "接收消息执行错误: " << WSAGetLastError() << std::endl; closesocket(clientSocket); WSACleanup(); return 1; } std::cout << "收到来着客户端发送的消息: " << recvBuffer << std::endl; closesocket(clientSocket); // 关闭客户端套接字 closesocket(listenSocket); // 关闭监听套接字 WSACleanup(); // 清理Winsock资源 return 0; }
运行效果:
4.2 创建TCP客户端
下面代码实现一个TCP客户端,连接到指定的服务器并完成通信。
#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库 int main() { WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息 int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值 if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序 return 1; } SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值 if (connectSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 WSACleanup(); //清除Winsock库 return 1; } sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息 service.sin_family = AF_INET; //指定地址族为IPv4 inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中 service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中 result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值 if (result == SOCKET_ERROR) { std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Connected to server." << std::endl; //连接成功,输出消息 char sendBuffer[1024] = "Hello, server!"; //创建发送缓冲区,存储待发送的数据 result = send(connectSocket, sendBuffer, sizeof(sendBuffer), 0); //向服务器发送数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "send failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据 result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 0; }
运行效果:
4.3 TCP客户端循环接收消息
#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #pragma comment(lib, "ws2_32.lib") //告诉编译器链接Winsock库 int main() { WSADATA wsaData; //创建一个结构体变量,用于存储关于Winsock库的信息 int result = WSAStartup(MAKEWORD(2, 2), &wsaData); //初始化Winsock库,指定版本号2.2,检查返回值 if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; //输出错误信息并退出程序 return 1; } SOCKET connectSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个TCP套接字,检查返回值 if (connectSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 WSACleanup(); //清除Winsock库 return 1; } sockaddr_in service; //创建一个结构体变量,用于存储服务器地址信息 service.sin_family = AF_INET; //指定地址族为IPv4 inet_pton(AF_INET, "127.0.0.1", &service.sin_addr); //将字符串类型的IP地址转换为二进制网络字节序的IP地址,并存储在结构体中 service.sin_port = htons(12345); //将端口号从主机字节序转换为网络字节序,并存储在结构体中 result = connect(connectSocket, (SOCKADDR*)&service, sizeof(service)); //连接到服务器,检查返回值 if (result == SOCKET_ERROR) { std::cout << "connect failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出程序 closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 1; } std::cout << "Connected to server." << std::endl; //连接成功,输出消息 char recvBuffer[1024]; //创建接收缓冲区,用于存储从服务器接收到的数据 while (true) { result = recv(connectSocket, recvBuffer, sizeof(recvBuffer), 0); //从服务器接收数据,检查返回值 if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; //输出错误信息并退出循环 break; } else if (result > 0) //判断是否有数据接收到 { std::cout << "Received message from server: " << recvBuffer << std::endl; //输出从服务器收到的数据 } else //连接断开 { std::cout << "Server disconnected." << std::endl; break; } } closesocket(connectSocket); //关闭套接字 WSACleanup(); //清除Winsock库 return 0; }
4.4 TCP服务器并发处理客户端请求
下面示例代码中,使用了
#include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <thread> #include <vector> #pragma comment(lib, "ws2_32.lib") // 处理客户端连接的函数 void HandleClient(SOCKET clientSocket) { char recvBuffer[1024]; int result; while (true) { result = recv(clientSocket, recvBuffer, sizeof(recvBuffer), 0); if (result == SOCKET_ERROR) { std::cout << "recv failed with error: " << WSAGetLastError() << std::endl; break; } else if (result > 0) { std::cout << "Received message from client: " << recvBuffer << std::endl; } else { std::cout << "Client disconnected." << std::endl; break; } } closesocket(clientSocket); } int main() { WSADATA wsaData; int result = WSAStartup(MAKEWORD(2, 2), &wsaData); if (result != 0) { std::cout << "WSAStartup failed: " << result << std::endl; return 1; } SOCKET listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (listenSocket == INVALID_SOCKET) { std::cout << "socket failed with error: " << WSAGetLastError() << std::endl; WSACleanup(); return 1; } sockaddr_in service; service.sin_family = AF_INET; service.sin_addr.s_addr = INADDR_ANY; service.sin_port = htons(12345); result = bind(listenSocket, (SOCKADDR*)&service, sizeof(service)); if (result == SOCKET_ERROR) { std::cout << "bind failed with error: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } result = listen(listenSocket, SOMAXCONN); if (result == SOCKET_ERROR) { std::cout << "listen failed with error: " << WSAGetLastError() << std::endl; closesocket(listenSocket); WSACleanup(); return 1; } std::cout << "Server is listening for incoming connections." << std::endl; std::vector<std::thread> threads; // 存储线程对象 while (true) { SOCKET clientSocket = accept(listenSocket, NULL, NULL); if (clientSocket == INVALID_SOCKET) { std::cout << "accept failed with error: " << WSAGetLastError() << std::endl; continue; } std::cout << "Client connected." << std::endl; // 创建一个新线程来处理客户端连接 std::thread thread(HandleClient, clientSocket); // 存储线程对象 threads.push_back(std::move(thread)); } // 等待所有线程执行完毕 for (auto& thread : threads) { thread.join(); } closesocket(listenSocket); WSACleanup(); return 0; }
运行效果: