C++ --- Socket套接字的使用
目录
一.什么是Socket套接字?
二.Socket的使用:
前置步骤:
为什么要加入 WSAStartup 和 WSACleanup ?
1.创建Socket:
2.绑定Socket:
3.服务端监听连接请求:
4.服务端接受客户端连接:
5.客户端连接服务端:
6.数据传输:
(1)recv的使用:
(2)send的使用:
7.关闭Socket:
8.案例分析:
三.TCP协议流程:
四.UDP协议流程:
前置知识:
UDP 的特性:
UDP的使用:
接收和发送数据的函数:
recvfrom() 函数
sendto() 函数
1.UDP的服务端:
2.UDP的客户端:
五.前置基础知识总结:
1. 网络基础知识:
2. Socket 的基本概念:
3. Socket 编程中的常用 API:
创建和管理 Socket:
发送和接收数据:
地址结构和网络信息:
4. 常见的 Socket 编程模式:
客户端-服务器模式:
广播和多播:
5. 网络调试和错误处理 :
下面是写博客时参考总结的博客地址:
1.从零开始的C++网络编程-腾讯云开发者社区-腾讯云
2.C++高性能网络编程 | Jack Huang's Blog
3.C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)-阿里云开发者社区
一.什么是Socket套接字?
Socket 是一种用于计算机之间网络通信的端点。它允许两个程序通过网络交换数据。Socket编程主要用于客户端和服务器的通信,一般可以使用两种协议:
- TCP (Transmission Control Protocol):可靠、面向连接的协议,适合需要完整传输的数据。TCP 是面向连接的协议,通信前需要通过三次握手(3-way handshake)建立连接。保证数据的可靠性,数据包丢失时会自动重传。通过滑动窗口等技术进行流量控制,避免网络拥塞。TCP 保证数据按顺序到达。
- UDP (User Datagram Protocol):不可靠、无连接的协议,适合对速度要求高、允许丢包的数据传输。UDP 是无连接的,不需要建立连接,数据可以直接发送。不保证数据的可靠送达,也不保证数据顺序。由于没有建立连接的开销,UDP 相比 TCP 更加高效,适合实时应用。
创建 socket 的时候需要指定 socket 的类型,一般有三种:
- SOCK_STREAM:面向连接的稳定通信,底层是 TCP 协议,我们会一直使用这个。
- SOCK_DGRAM:无连接的通信,底层是 UDP 协议,需要上层的协议来保证可靠性。
- SOCK_RAW:更加灵活的数据控制,能让你指定 IP 头部
想了解前置知识可以跳转五 -->
二.Socket的使用:
下面我们基于TCP协议来详细讲述Socket的使用。
我们在编写C++程序运行后代表的是服务端,浏览器则是客户端:首先创建一个套接字(socket()),绑定(bind())到本地的IP地址和端口,然后进入监听(listen())状态,服务端接受(accept())客户端,客户端连接(connect)服务端。连接(connect)成功后,可以接收(recv)客户端数据和发送(send)数据给客户端。通信完成后,关闭(close())Socket释放资源。
前置步骤:
我们在 Windows 平台上使用 Winsock 编程(即使用 Socket 进行网络通信)时,必须初始化 Winsock 库并在使用结束后清理它。
#include <iostream>
#include <string.h>
#include <WinSock2.h> // 声明了所有与 Windows 套接字(Socket)相关的函数和数据结构。WSADATA 结构体以及 WSAStartup、WSACleanup 函数也在此头文件中定义。
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
// 初始化 Winsock 库
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed: " << WSAGetLastError() << endl;
return -1;
}
//socket流程...
// 释放Winsock的资源
WSACleanup();
}
MAKEWORD(2, 2)
表示请求使用版本 2.2 的 Winsock API,&wsaData
是一个指向WSADATA
结构体的指针,Winsock 会将相关信息存储在这个结构体中。- 如果初始化失败,
WSAStartup
会返回非零值,我们可以使用WSAGetLastError()
获取错误代码。
为什么要加入
WSAStartup
和WSACleanup
?Winsock 是 Windows 的网络编程接口,它为 Windows 操作系统提供了网络应用程序接口(API),允许程序通过 TCP/IP 协议族进行通信。在 Windows 中使用 Socket 之前,必须首先调用
WSAStartup
来初始化 Winsock 库。它的作用是让应用程序和 Winsock 层的网络操作系统组件建立联系。WSAStartup
会检查 Winsock 库的版本是否与系统兼容,并加载网络功能所需的组件。每个成功调用WSAStartup
的应用程序,在程序结束时都需要调用WSACleanup
来清理 Winsock 使用的资源。它会关闭网络资源并释放内存等,以防止内存泄漏或其他资源问题。
这个步骤与在其他操作系统(如 Linux)中进行网络编程时的步骤不同,因为 Linux 不需要这样的显式初始化操作。Linux 的网络 API 是直接通过系统调用实现的,使用 socket
和相关函数时不需要先初始化网络库。
1.创建Socket:
要创建一个Socket,需要使用socket()
函数。
SOCKET socket(int domain, int type, int protocol);
domain
:通信协议族,AF_INET
用于IPv4,AF_INET6
用于IPv6。type
:指定Socket类型,SOCK_STREAM
用于TCP,SOCK_DGRAM
用于UDP。protocol
:一般设置为0
即可,由系统自动选择协议。
domain参数
该参数指明要创建的sockfd的协议族,一般比较常用的有两个:
AF_INET
:IPv4协议族AF_INET6
:IPv6协议族
type参数
该参数用于指明套接字类型,具体有:
SOCK_STREAM
:字节流套接字,适用于TCP或SCTP协议SOCK_DGRAM
:数据报套接字,适用于UDP协议SOCK_SEQPACKET
:有序分组套接字,适用于SCTP协议SOCK_RAW
:原始套接字,适用于绕过传输层直接与网络层协议(IPv4/IPv6)通信
protocol参数
该参数用于指定协议类型。
如果是TCP协议的话就填写
IPPROTO_TCP
,UDP和SCTP协议类似。也可以直接填写0,这样的话则会默认使用
domain
参数和type
参数组合制定的默认协议(参照上面type参数的适用协议)
返回值
socket
函数在成功时会返回套接字描述符,失败则返回-1。失败的时候可以通过输出errno
来详细查看具体错误类型。
例如下面的代码创建了一个TCP套接字,如果失败会返回负值:
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
// 当 sockfd = -1 则代表socket创建失败
if (sockfd < 0) {
perror("Socket creation failed");
cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;
return -1;
}
在底层C++使用两个宏来表示 socket 创建状态:
#define INVALID_SOCKET (SOCKET)(~0)
#define SOCKET_ERROR (-1)
2.绑定Socket:
服务端的Socket需要绑定到指定的IP地址和端口,以便客户端可以连接。绑定可以通过bind()
函数来完成,把一个本地协议地址赋予一个套接字。
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:Socket描述符,即前一步创建的Socket。addr
:结构体sockaddr
指针,包含IP地址和端口信息。addrlen
:addr
结构体的大小。
例如下面这段代码,将Socket绑定到本地IP地址和端口8080上:
首先看一下sockaddr_in的底层,我们一般根据这个结构体的底层的其中三个字段进行赋值然后与创建的套接字进行绑定:
struct sockaddr_in { uint8_t sin_len; // 结构长度,非必需 short sin_family; // 地址族,一般为AF_****格式,常用的是AF_INET USHORT sin_port; // 16位TCP或UDP端口号 IN_ADDR sin_addr; // 32位IPv4地址 CHAR sin_zero[8]; // 保留数据段,一般置零 };
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080); // 指定端口,大小端问题,将本地转换成路由器使用的大端(千百十个)
server_address.sin_addr.s_addr = INADDR_ANY; // 绑定到本地所有可用的IP
// server_address.sin_addr.s_addr = inet_addr("0.0.0.0"); // 字符串IP地址转换成整数IP
// 返回-1代表绑定错误
if (bind(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) < 0) {
perror("Bind failed");
cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;
// 关闭连接
closesocket(sockfd);
return -1;
}
不同的计算机对数据的存储格式不一样,比如 32 位的整数 0x12345678,可以在内存里从高到低存储为 12-34-56-78 (大端)或者从低到高存储为 78-56-34-12(小端)。但是这对于网络中的数据来说就带来了一个严重的问题,当机器从网络中收到 12-34-56-78 的数据时,它怎么知道这个数据到底是什么意思?
解决的方案也比较简单,在传输数据之前和接受数据之后,必须调用 htonl/htons 或 ntohl/ntohs 先把数据转换成网络字节序或者把网络字节序转换为机器的字节序。
我们注意到上面代码不管是赋值IP还是端口,都不是直接赋值,而是使用了类似
htons()
或htonl()
的函数,这便是字节排序函数。不同的机子上对于多字节变量的字节存储顺序是不同的,有大端字节序和小端字节序两种。如果我们将机子A的变量原封不动传到机子B上,其值可能会发生变化,导致数据传输异常。故我们需要引入一个通用的规范,称为网络字节序。
#include <WinSock2.h> uint16_t htons(uint16_t host16bitvalue); //host to network, 16bit uint32_t htonl(uint32_t host32bitvalue); //host to network, 32bit uint16_t ntohs(uint16_t net16bitvalue); //network to host, 16bit uint32_t ntohl(uint32_t net32bitvalue); //network to host, 32bit
3.服务端监听(listen)连接请求:
我们在绑定成功后,服务端Socket需要进入监听模式,以便等待客户端的连接请求。监听通过listen()
函数实现。
int listen(int sockfd, int backlog);
sockfd
:Socket描述符。backlog
:等待连接的最大队列长度。
例如下面的代码将Socket设置为监听模式,并允许最多3个客户端等待连接:
if (listen(sockfd, 3) < 0) {
perror("Listen failed");
cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;
// 关闭连接
closesocket(sockfd);
return -1;
}
4.服务端接受(accept)客户端连接:
当客户端尝试连接时,服务端通过accept()
函数接受连接。
SOCKET accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
sockfd
:服务端的监听Socket。addr
:指向存储客户端信息的sockaddr
结构体。addrlen
:addr
结构体的大小。
例如下面代码,accept()是一个阻塞函数,
会阻塞程序,直到有客户端连接。如果连接成功,会返回新的Socket用于与该客户端通信:
struct sockaddr_in client_address;
socklen_t client_address_len = sizeof(client_address);
while (1) {
// 这里返回客户端的new_socket才是跟客户端可通讯的socket
SOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, &client_address_len);
if (new_socket == INVALID_SOCKET) {
perror("Accept failed");
cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;
// 直到创建可通讯的socket才能跳出循环
continue;
}
// 开始通讯
// 关闭连接
}
5.客户端连接(connect)服务端:
客户端通过connect()
函数向服务端发起连接。在调用connect函数的时候,调用方(也就是客户端)便会主动发起TCP三次握手。
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
sockfd
:客户端的Socket。addr
:指向服务端地址的sockaddr
结构体。addrlen
:addr
结构体的大小。
在操作上比较类似于服务端使用bind函数(虽然做的事情完全不一样),唯一的区别在于指定ip这块。服务端调用bind函数的时候无需指定ip,但客户端调用connect函数的时候则需要指定服务端的ip。在客户端的代码中,令套接字地址结构体指定ip的代码如下:
inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr);
这个就涉及到ip地址的表达格式与数值格式相互转换的函数。
IP地址格式转换函数
IP地址一共有两种格式:
- 表达格式:也就是我们能看得懂的格式,例如
"192.168.19.12"
这样的字符串- 数值格式:可以存入套接字地址结构体的格式,数据类型为整型
显然,当我们需要将一个IP赋进套接字地址结构体中,就需要将其转换为数值格式。#include <WinSock2.h>提供了两个函数用于IP地址格式的相互转换:
// 将IP地址从表达格式转换为数值格式 int inet_pton(int domain, const char *strptr, void *addrptr); // 将IP地址从数值格式转换为表达格式 const char *inet_ntop(int domain, const void *addrptr, char *strptr, size_t len);
下面代码的作用是将客户端连接到本地地址127.0.0.1
的端口8080上:
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &server_address.sin_addr);
if (connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1) {
printf("Connect error(%d): %s\n", errno, strerror(errno));
closesocket(sockfd);
return -1;
}
6.数据传输:
一旦建立连接,客户端和服务端就可以使用send()
和recv()
来交换数据。
send()
:用于发送数据。recv()
:用于接收数据。
while (1) {
struct sockaddr_in client_address;
int client_address_len = sizeof(client_address);
// 这里返回客户端的new_socket才是跟客户端可通讯的socket
SOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, client_address_len);
if (new_socket == INVALID_SOCKET) {
perror("Accept failed");
cout << "create listen socket failed !!! errcode: " + GetLastError() << endl;
// 直到创建可通讯的socket才能跳出循环
continue;
}
// 开始通信(B/S)
char buffer[1024] = { 0 };
recv(new_socket,buffer,1024,0);
// 关闭连接
closesocket(new_socket);
}
客户端发送请求,服务器接收请求并返回响应。由于 TCP 是面向流的协议,
recv
和send
需要处理流式数据。数据可能会被分割成多个包发送,也可能会接收到部分数据,因此需要处理数据的拼接和分包。recv
和send
在默认情况下是阻塞的,调用者会等待数据的发送或接收完成。通过设置MSG_DONTWAIT
等标志可以实现非阻塞模式。
(1)recv的使用:
下面是recv()的底层代码:
recv(
SOCKET s, // 客户端socket
char* buf, // 接受的数据存到哪里
int len, // 接受的长度
int flags // 标记0
);
- 如果发送成功,返回实际发送的字节数。如果发送的数据量大于缓冲区大小,返回的值可能小于
len
,此时需要调用send
发送剩余的数据。- 如果发生错误,返回
-1
,并且设置errno
,可以使用perror()
或strerror()
获取错误信息。
(2)send的使用:
send(
SOCKET s,
const char* buf,
int len,
int flags
);
- sockfd:表示一个已经建立连接的Socket描述符。你可以通过调用
socket()
创建一个Socket并通过connect()
(客户端)或accept()
(服务器)建立连接。- buf:一个指向数据的指针,表示要发送的数据缓冲区。通常是一个字符数组或字符串。
- len:要发送的字节数。这个长度指的是缓冲区中的数据的大小。
- flags:发送时的标志。常见的标志有:
0
:表示默认行为。MSG_DONTWAIT
:在非阻塞模式下使用,表示如果没有数据可以发送,send()
函数将立刻返回,而不是阻塞。MSG_NOSIGNAL
:如果套接字已关闭,不会抛出信号(只适用于某些平台,如Linux)。
7.关闭(closesocket)Socket:
通信完成后,需要关闭Socket释放资源。可以使用closesocket()
函数来关闭Socket。
closesocket(sockfd);
8.案例分析:
编写服务端代码,能够使打开浏览器访问http://127.0.0.1:8080/
,可以显示“Hello, World!”。
TCP 服务器需要以下步骤:
- 创建 Socket。
- 绑定 Socket 到一个特定的地址和端口。
- 监听端口,等待客户端连接。
- 接受客户端连接并处理数据。
- 关闭连接。
#include <iostream>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
using namespace std;
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed: " << WSAGetLastError() << endl;
return -1;
}
// 创建Socket套接字
SOCKET sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == INVALID_SOCKET) {
cerr << "Socket creation failed: " << WSAGetLastError() << endl;
WSACleanup();
return -1;
}
// 绑定Socket到IP和端口
struct sockaddr_in server_address = { 0 };
server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr("0.0.0.0");
server_address.sin_port = htons(8080);
if (bind(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) == SOCKET_ERROR) {
cerr << "Bind failed: " << WSAGetLastError() << endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
// 开始监听
if (listen(sockfd, 3) < 0) {
cerr << "Listen failed: " << WSAGetLastError() << endl;
closesocket(sockfd);
WSACleanup();
return -1;
}
cout << "Waiting for connections on port 8080...\n";
while (true) {
struct sockaddr_in client_address;
int client_address_len = sizeof(client_address);
// 接受客户端连接
SOCKET new_socket = accept(sockfd, (struct sockaddr*)&client_address, &client_address_len);
if (new_socket == INVALID_SOCKET) {
cerr << "Accept failed: " << WSAGetLastError() << endl;
continue;
}
// 接收客户端请求
char buffer[1024] = { 0 };
recv(new_socket, buffer, sizeof(buffer), 0);
cout << "Received request:\n" << buffer << endl;
// 构造并发送简单的HTTP响应,包括状态行、响应头和响应体(内容为“Hello, World!”)
const char* http_response =
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html\r\n"
"Content-Length: 13\r\n"
"\r\n"
"Hello, World!";
send(new_socket, http_response, strlen(http_response), 0);
// 关闭连接
closesocket(new_socket);
}
// 清理Socket
closesocket(sockfd);
WSACleanup();
return 0;
}
编写客户端代码:
TCP 客户端需要以下步骤:
- 创建 Socket。
- 连接到服务器的 IP 地址和端口。
- 发送数据给服务器。
- 接收来自服务器的响应。
- 关闭连接。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed: " << WSAGetLastError() << endl;
return -1;
}
// 创建 Socket
SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (clientSocket == INVALID_SOCKET) {
cerr << "Socket creation failed: " << WSAGetLastError() << endl;
WSACleanup();
return -1;
}
// 设置服务器地址
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080); // 连接到服务器的 8080 端口
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址:本地地址
// 连接到服务器
if (connect(clientSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
cerr << "Connection failed: " << WSAGetLastError() << endl;
closesocket(clientSocket);
WSACleanup();
return -1;
}
cout << "Connected to server." << endl;
// 发送数据
const char* message = "Hello, Server!";
if (send(clientSocket, message, strlen(message), 0) == SOCKET_ERROR) {
cerr << "Send failed: " << WSAGetLastError() << endl;
closesocket(clientSocket);
WSACleanup();
return -1;
}
// 接收响应
char buffer[1024] = {0};
int bytesReceived = recv(clientSocket, buffer, sizeof(buffer), 0);
if (bytesReceived > 0) {
cout << "Server says: " << buffer << endl;
} else if (bytesReceived == 0) {
cout << "Connection closed by server." << endl;
} else {
cerr << "Receive failed: " << WSAGetLastError() << endl;
}
// 关闭连接
closesocket(clientSocket);
WSACleanup();
return 0;
}
三.TCP协议流程:
TCP 协议是面向连接的,它在客户端和服务器之间建立一个可靠的连接。具体流程如下:
TCP三次握手(建立连接):
- 客户端向服务器发送一个 SYN 请求包,表示要建立连接。
- 服务器收到后,回复一个 SYN-ACK 包,表示同意连接。
- 客户端收到后,发送一个 ACK 包,确认连接建立。
A(客户端): CLOSED -> SYN_SENT -> ESTABLISHED
B(服务端): LISTEN(监听状态) -> SYN_RCVD -> ESTABLISHED
首先,客户端主动调用connect发送一个SYN包(如TCP首部和TCP选项等协议包必须数据)来对服务端请求连接,此时客户端的状态从CLOSED转换成SYN_SENT。
在socket编程中,服务端调用过listen函数使其处于监听状态并且处于accept函数等待连接的阻塞状态下,才能收到SYN包返回一个针对该SYN包的响应包(ACK包)和一个新的SYN包,此时服务端状态由LISTEN转换成SYN_RCVD。
随后客户端收到服务端发来的两个包,并返回针对新的SYN包的ACK包。此时客户端的状态从SYN_SENT切换至ESTABLISHED,处于这个状态的客户端就可以传输数据了。
服务端收到ACK包后代表成功建立连接,这时调用accept函数返回客户端套接字,同样服务端的状态由SYN_RCVD切换至ESTABLISHED,同样处于这个状态的服务端就可以传输数据了。
数据传输:
一旦连接建立,客户端和服务器就可以通过 send() 和 recv() 来发送和接收数据
TCP四次挥手(断开连接):
- 客户端或服务器中的任一方可以发送 FIN 包,表示断开连接。
- 对方收到后,会回复一个 ACK 包确认断开。
- 最后,发送 FIN 包的方再收到对方的 ACK 包,连接彻底关闭。
假设A主动关闭连接,A与B的流程转换图如下:
A: ESTABLISHED -> FIN_WAIT_1 -> FIN_WAIT_2 -> TIME_WAIT -> CLOSED
B:ESTABLISHED -> CLOSE_WAIT -> LAST_ACK -> CLOSED
在收发数据之后,如果需要断开连接,方中有一方(假设为A,另一方为B)主动关闭连接(调用close函数或者其进程本身被终止等情况)则其向B发送FIN包。此时A的状态从ESTABLISHED转换成FIN_WAIT_1。
B接收到A传递的FIN包后发送ACK包,此时B的状态从ESTABLISHED转换成CLOSE_WAIT。
随后A接收到B传递的ACK包后,此时A的状态从FIN_WAIT_1转换成FIN_WAIT_2。
过一段时间后,B调用close函数发送FIN包,此时B的状态从CLOSE_WAIT转换成LAST_ACK。
A接收到FIN包并发送ACK包,此时A的状态由FIN_WAIT_2转换成TIME_WAIT。
B接收到ACK包后关闭连接,此时B的状态从LAST_ACK转换成CLOSED。
A等待一段时间后关闭连接,此时A的状态从TIME_WAIT转换成CLOSED状态。
四.UDP协议流程:
UDP(用户数据报协议)是一种无连接的网络协议,与 TCP 相比,UDP 不会建立连接并且不保证数据的可靠性,适用于需要高速传输且容忍丢包的场景。UDP 编程相对简单,因为不需要像 TCP 一样进行连接和握手,但也因为没有连接管理,数据可能会丢失。
前置知识:
UDP 的特性:
UDP 不需要在发送数据前建立连接,这使得它比 TCP 更加高效,尤其是在大量数据传输时。但UDP 不保证数据的可靠送达,因此不会进行重传、流量控制和拥塞控制。如果需要可靠性,应用层需要自己处理。UDP 发送的数据包可能会乱序接收,应用层需要处理顺序问题。
UDP的使用:
UDP 是无连接的,它不需要像 TCP 那样进行握手,适合需要高效传输且容忍数据丢失的应用场景。UDP 不需要像 TCP 那样进行三次握手建立连接,也没有四次挥手断开连接。每次通信都是独立的,发送方和接收方都可以在任意时间发送和接收数据包。在 C++ 中使用 UDP 编程时,创建 Socket 使用
SOCK_DGRAM
类型,数据的发送和接收使用sendto()
和recvfrom()
,不需要使用connect()
。UDP没有可靠性保证,应用程序需要自行确保数据的完整性和顺序,或者使用更高层的协议来处理。
接收和发送数据的函数:
在 Socket 编程中,recvfrom()
和 sendto()
是 UDP 协议下用于接收和发送数据的函数。这些函数是 UDP 套接字通信的关键,它们支持无连接的数据传输,允许在网络上发送和接收数据报(Datagram)。这两者是 UDP 套接字编程中最常用的函数,因为它们简化了数据传输的过程,并支持多播和广播通信。
recvfrom()
函数
recvfrom()
是用于接收来自指定源(IP 地址和端口)的数据报的函数。在 UDP 通信中,数据报是独立的、无连接的,这意味着每个数据包可以有不同的来源和目标。int recvfrom( SOCKET s, // 套接字描述符 char *buf, // 缓冲区,用于存储接收到的数据 int len, // 缓冲区的大小 int flags, // 接收标志,通常为 0 struct sockaddr *from, // 来源地址(接收到数据报的来源地址) int *fromlen // 来源地址的长度 );
示例:UDP 服务器端使用
recvfrom():
recvfrom()
接收数据并存储在buffer
中。clientAddr
保存了发送者的 IP 地址和端口号。bytesReceived
记录了成功接收的字节数。char buffer[1024]; sockaddr_in clientAddr; int clientAddrSize = sizeof(clientAddr); // 接收数据 int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (sockaddr*)&clientAddr, &clientAddrSize); if (bytesReceived == SOCKET_ERROR) { cerr << "Receive failed: " << WSAGetLastError() << endl; } else { cout << "Received: " << buffer << endl; }
sendto()
函数
sendto()
用于向指定的目标地址发送数据报。由于 UDP 是无连接的协议,数据包发送时需要显式地指定目标地址和端口。int sendto( SOCKET s, // 套接字描述符 const char *buf, // 需要发送的数据 int len, // 数据的长度 int flags, // 发送标志,通常设为 0 const struct sockaddr *to, // 目标地址(发送数据报的目标地址) int tolen // 目标地址的长度 );
示例:UDP 客户端使用
sendto():
sendto()
将消息发送到127.0.0.1
上的端口 8080。目标地址是serverAddr
,它包含了目标服务器的 IP 地址和端口号。const char* message = "Hello, Server!"; sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons(8080); serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 发送数据 int bytesSent = sendto(clientSocket, message, strlen(message), 0, (sockaddr*)&serverAddr, sizeof(serverAddr)); if (bytesSent == SOCKET_ERROR) { cerr << "Send failed: " << WSAGetLastError() << endl; } else { cout << "Message sent to server." << endl; }
1.UDP的服务端:
UDP 服务器端的主要工作是:
- 创建一个 Socket。
- 绑定(bind) Socket 到一个指定的端口。
- 接收来自客户端的数据(没有连接过程)。
- 发送数据回客户端(如果需要)。
- 初始化 Winsock:调用
WSAStartup()
初始化 Winsock 库。- 创建 UDP Socket:使用
socket()
创建一个 UDP Socket(SOCK_DGRAM
表示数据报类型的套接字)。- 设置服务器地址:设置服务器的 IP 地址和端口,使用
INADDR_ANY
来绑定所有网络接口。- 绑定 Socket:通过
bind()
将 Socket 绑定到指定的地址和端口。- 接收数据:通过
recvfrom()
接收来自客户端的数据,不需要建立连接,因为 UDP 是无连接的。接收的数据会被存放在buffer
中,clientAddr
用来保存客户端的地址信息。- 发送响应:使用
sendto()
向客户端发送数据。由于是无连接的,每次发送时都需要指定客户端的地址信息。- 关闭 Socket:程序结束后通过
closesocket()
关闭 Socket。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main() {
// 调用 WSAStartup() 初始化 Winsock 库。
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed: " << WSAGetLastError() << endl;
return -1;
}
// 使用 socket() 创建一个 UDP Socket(SOCK_DGRAM 表示数据报类型的套接字)。
SOCKET serverSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serverSocket == INVALID_SOCKET) {
cerr << "Socket creation failed: " << WSAGetLastError() << endl;
WSACleanup();
return -1;
}
// 设置服务器的 IP 地址和端口,使用 INADDR_ANY 来绑定所有网络接口。
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080); // 设置端口
serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); // 绑定所有网络接口
// 绑定 Socket,通过 bind() 将 Socket 绑定到指定的地址和端口
if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
cerr << "Bind failed: " << WSAGetLastError() << endl;
closesocket(serverSocket);
WSACleanup();
return -1;
}
cout << "Server listening on port 8080..." << endl;
// 接收数据
char buffer[1024] = {0};
sockaddr_in clientAddr;
int clientAddrSize = sizeof(clientAddr);
while (true) {
// 通过 recvfrom() 接收来自客户端的数据,不需要建立连接,因为 UDP 是无连接的。
// 接收的数据会被存放在 buffer 中,clientAddr 用来保存客户端的地址信息。
int bytesReceived = recvfrom(serverSocket, buffer, sizeof(buffer), 0, (sockaddr*)&clientAddr, &clientAddrSize);
if (bytesReceived == SOCKET_ERROR) {
cerr << "Receive failed: " << WSAGetLastError() << endl;
continue;
}
cout << "Received: " << buffer << endl;
// 向客户端发送响应
const char* response = "Message received!";
// 使用 sendto() 向客户端发送数据。由于是无连接的,每次发送时都需要指定客户端的地址信息。
int bytesSent = sendto(serverSocket, response, strlen(response), 0, (sockaddr*)&clientAddr, sizeof(clientAddr));
if (bytesSent == SOCKET_ERROR) {
cerr << "Send failed: " << WSAGetLastError() << endl;
}
}
// 程序结束后通过 closesocket() 关闭 Socket。
closesocket(serverSocket);
// 释放Winsock资源
WSACleanup();
return 0;
}
2.UDP的客户端:
UDP 客户端的主要工作是:
- 创建一个 Socket。
- 向服务器发送数据。
- 接收来自服务器的响应。
- 初始化 Winsock:同样需要初始化 Winsock。
- 创建 UDP Socket:创建一个 UDP 类型的 Socket。
- 设置服务器地址:设置服务器的 IP 地址和端口。
- 发送数据:使用
sendto()
向服务器发送数据。UDP 不需要建立连接,因此发送时直接指定服务器的地址。- 接收数据:通过
recvfrom()
接收来自服务器的响应。- 关闭 Socket:最后关闭客户端的 Socket。
#include <iostream>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main() {
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
cerr << "WSAStartup failed: " << WSAGetLastError() << endl;
return -1;
}
// 创建 UDP Socket
SOCKET clientSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (clientSocket == INVALID_SOCKET) {
cerr << "Socket creation failed: " << WSAGetLastError() << endl;
WSACleanup();
return -1;
}
// 设置服务器地址
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(8080); // 服务器端口号
serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); // 服务器地址
// 发送数据
const char* message = "Hello, Server!";
int bytesSent = sendto(clientSocket, message, strlen(message), 0, (sockaddr*)&serverAddr, sizeof(serverAddr));
if (bytesSent == SOCKET_ERROR) {
cerr << "Send failed: " << WSAGetLastError() << endl;
closesocket(clientSocket);
WSACleanup();
return -1;
}
// 接收响应
char buffer[1024] = {0};
sockaddr_in serverResponseAddr;
int serverResponseAddrSize = sizeof(serverResponseAddr);
int bytesReceived = recvfrom(clientSocket, buffer, sizeof(buffer), 0, (sockaddr*)&serverResponseAddr, &serverResponseAddrSize);
if (bytesReceived == SOCKET_ERROR) {
cerr << "Receive failed: " << WSAGetLastError() << endl;
} else {
cout << "Server says: " << buffer << endl;
}
// 关闭 Socket
closesocket(clientSocket);
WSACleanup();
return 0;
}
五.前置基础知识总结:
详细知识可参考下面文档:C/C++网络编程基础知识超详细讲解第一部分(系统性学习day11)-阿里云开发者社区
1. 网络基础知识:
- IP 地址:计算机在网络中的唯一标识。
- 端口号:应用程序通过端口号与网络中的其他计算机进行通信。
- 协议:通信协议用于规范计算机间如何传输数据,最常用的协议有 TCP 和 UDP。
- 路由和网络层次结构:数据如何在不同的网络设备之间转发,网络中的路由和地址解析协议(如 ARP)是如何工作的。
- 网络模型:OSI 七层模型和 TCP/IP 四层模型。
- OSI 模型:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
- TCP/IP 模型:网络接口层、互联网层、传输层、应用层。
2. Socket 的基本概念:
Socket 是网络通信的接口,是操作系统提供的用于建立网络通信的抽象层。需要了解以下概念:
- 套接字(Socket):是应用程序与网络进行通信的端点。
- Socket 类型:常见的有:
- 流式套接字(SOCK_STREAM):基于 TCP 协议,保证数据传输可靠,适用于需要高可靠性的应用。
- 数据报套接字(SOCK_DGRAM):基于 UDP 协议,不保证数据的可靠传输,适用于快速、简单的通信。
- 协议族(Protocol Family):通常使用
AF_INET
(IPv4)或AF_INET6
(IPv6)等。- 连接类型:
- 面向连接的(Connection-oriented):如 TCP,传输之前需要建立连接。
- 无连接的(Connectionless):如 UDP,数据包在发送时不需要先建立连接。
3. Socket 编程中的常用 API:
在学习 Socket 编程时,我们会用到很多函数来创建、绑定、监听、发送、接收、关闭套接字等。以下是常用的 Socket 编程函数:
创建和管理 Socket:
socket()
:创建一个套接字。bind()
:将套接字与本地地址(IP 地址和端口)绑定。listen()
:在服务器端用于监听端口,准备接受连接。accept()
:在服务器端接受客户端的连接请求。connect()
:在客户端连接到服务器端。close()
:关闭套接字。
发送和接收数据:
send()
:在已连接的 TCP 套接字上发送数据。recv()
:从已连接的 TCP 套接字接收数据。sendto()
:在 UDP 套接字上发送数据,需要指定目标地址。recvfrom()
:从 UDP 套接字接收数据,返回数据源的地址信息。
地址结构和网络信息:
sockaddr_in
:用于指定 IPv4 地址和端口。gethostbyname()
:根据域名获取 IP 地址。inet_pton()
和inet_ntop()
:用于在点分十进制(Dotted Decimal)和二进制表示之间转换 IP 地址。
4. 常见的 Socket 编程模式:
客户端-服务器模式:
服务器端等待客户端的连接,客户端向服务器发起连接请求,建立起通信后交换数据。
广播和多播:
UDP 支持广播和多播通信:
- 广播:发送数据包到网络中的所有主机(指定广播地址)。
- 多播:发送数据包到特定的组播地址,只有加入该组的主机才能接收到数据。
5. 网络调试和错误处理 :
在编写 Socket 程序时,调试和处理错误是非常重要的:
- 错误处理:每个 Socket 函数调用后,都应该检查其返回值,如果返回错误,需要根据
errno
或WSAGetLastError()
获取错误信息。- 网络工具:如
ping
、traceroute
、netstat
等工具帮助检查网络连接状况。