socket套接字及TCP、UDP的实现
socket套接字
Socket(套接字)是网络编程的核心概念,用于实现网络上不同设备或进程之间的双向数据通信。在不同的计算机之间进行通信时,Socket提供了一种标准化的接口,隐藏了底层的复杂协议,使网络编程更加方便。下面将从概念、类型、通信流程、编程接口以及实际应用等方面深入讲解Socket套接字。
socket 的基本概念
Socket(套接字):Socket是一种网络编程接口,通过它,程序可以在网络上接收或发送数据,通常由 IP 地址和端口号一起使用来标识特定的通信端点。
IP 地址:标识网络中唯一的设备位置。
端口号:标识网络中同一设备上运行的不同服务或应用。结合IP地址和端口号,Socket提供了唯一标识的通信端点。
每个socket可以看成一个通道,两端分别是通信双方,可以是客户端和服务器端。
socket的类型
Socket根据其支持的协议和特性不同,有多种类型,常见的有:
-
流式套接字(
SOCK_STREAM
):
基于TCP
协议:提供可靠的、面向连接的通信。
特点:数据传输有序、可靠,传输过程中的数据包不会丢失。
应用场景:适合需要高可靠性的应用,如文件传输、HTTP、数据库访问等。 -
数据报套接字(
SOCK_DGRAM
):
基于UDP
协议:提供无连接的数据报通信。
特点:数据传输无序、不保证可靠性(可能会丢失或乱序)。
应用场景:适合对实时性要求高但对数据可靠性要求低的应用,如视频、音频流传输、DNS查询等。 -
原始套接字(
SOCK_RAW
):
特点:允许直接访问底层协议(如IP协议),对网络报文进行完全控制。
应用场景:网络调试、开发网络协议、数据包捕获(如Wireshark)等低层操作。 -
流控套接字(
SOCK_SEQPACKET
):
提供有序和可靠的消息传输(结合了SOCK_STREAM
的可靠性和SOCK_DGRAM
的面向数据报特性),常用于实时通信。
Socket的工作流程
网络通信通常包括服务器和客户端,以下是Socket在建立连接和通信中的一般步骤,以常用的TCP连接为例:
服务器端工作流程:
- 创建Socket:调用 socket() 函数创建一个套接字对象。
- 绑定 IP 和端口:使用 bind() 函数将套接字绑定到指定的IP地址和端口号,成为一个通信端口。
- 监听连接:调用 listen() 函数,使服务器端Socket进入监听状态,等待客户端的连接请求。
- 接受连接:使用 accept() 函数接收客户端的连接请求,成功建立连接后生成一个新的套接字,用于与客户端通信。
- 数据传输:服务器与客户端之间可以使用 send() 和 recv() 函数进行数据传输。
- 关闭Socket:使用 close() 函数关闭套接字,释放资源。
客户端工作流程:
- 创建Socket:调用 socket() 函数创建一个套接字对象。
- 连接服务器:使用 connect() 函数向服务器发出连接请求。
- 数据传输:客户端和服务器端进行数据传输。
- 关闭Socket:使用 close() 函数关闭套接字,释放资源。
Socket的编程接口(C++ 示例)
在 C/C++ 网络编程中,使用一组标准的 API 来管理和处理网络通信。通过这些 API,程序可以创建网络连接、发送和接收数据。主要的网络编程 API 包括 创建 Socket、绑定地址、监听和接收连接、数据传输和关闭连接 等。下面详细介绍这些常用的 API 函数及其作用。
1. 创建 Socket
int socket(int domain, int type, int protocol);
作用:用于创建一个新的套接字(Socket)。
参数:
domain
:地址族,指定使用的协议族,AF_INET对应IPv4,AF_INET6对应IPv6。
type
:套接字类型,指定通信的特性,SOCK_STREAM:基于 TCP 协议的流式套接字,SOCK_DGRAM:基于 UDP 协议的数据报套接字。
protocol
:协议类型,一般为 0(自动选择适合的协议)。
返回值
:成功时返回套接字的文件描述符,失败时返回 -1。
示例:
int sockfd = socket(AF_INET, SOCK_STREAM, 0); //IPV4 TCP
if (sockfd == -1) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
2. 绑定地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:将套接字绑定到特定的 IP 地址和端口号,使其可以接收发往该地址和端口的连接或数据包。
参数:
sockfd
:套接字描述符(由 socket 返回)。
addr
:指向 sockaddr 结构体的指针,包含服务器 IP 地址和端口号,。
addrlen
:addr 的大小。
返回值
:成功时返回 0,失败时返回 -1。
示例:
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
address.sin_port = htons(8080); // 绑定到端口 8080
if (bind(sockfd, (struct sockaddr*)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
3. 监听连接
int listen(int sockfd, int backlog)
作用:使套接字进入监听状态,准备接收客户端的连接请求。
参数:
sockfd
:服务器的套接字描述符。
backlog
:等待连接队列的最大长度。
返回值
:成功时返回 0,失败时返回 -1。
示例:
if (listen(sockfd, 3) < 0) {
perror("listen failed");
exit(EXIT_FAILURE);
}
4. 接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
作用:从连接队列中取出一个客户端连接,返回新的套接字用于通信。
参数:
sockfd
:服务器的监听套接字。
addr
:指向 sockaddr 结构体的指针,存储客户端的 IP 地址和端口。
addrlen
:addr 的大小。
返回值
:成功时返回新的套接字描述符,失败时返回 -1。
示例:
int new_socket = accept(sockfd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("accept failed");
exit(EXIT_FAILURE);
}
5. 连接到服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
作用:客户端使用此函数向服务器发起连接请求。
参数:
sockfd
:客户端的套接字描述符。
addr
:服务器的 sockaddr 结构体,包含 IP 和端口。
addrlen
:addr 的大小。
返回值
:成功时返回 0,失败时返回 -1。
示例:
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
perror("connect failed");
exit(EXIT_FAILURE);
}
6. 发送数据
int send(int sockfd, const void *buffer, size_t length, int flags)
作用:用于向连接的另一端发送数据。
参数:
sockfd
:套接字描述符。
buffer
:要发送的数据缓冲区。
length
:缓冲区的大小。
flags
:传输标志,通常设为 0。
返回值
:成功时返回发送的字节数,失败时返回 -1。
示例:
const char *message = "Hello from client";
send(sockfd, message, strlen(message), 0);
7. 接收数据
int recv(int sockfd, void *buffer, size_t length, int flags)
作用:从连接的另一端接收数据。
参数:
sockfd
:套接字描述符。
buffer
:接收数据的缓冲区。
length
:缓冲区的大小。
flags
:传输标志,通常设为 0。
返回值
:成功时返回接收的字节数,失败时返回 -1。
示例:
char buffer[1024] = {0};
int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
printf("Received: %s\n", buffer);
8. 关闭 Socket
int close(int sockfd)
作用:关闭套接字,释放资源。
参数:
sockfd
:要关闭的套接字描述符。
返回值:成功时返回 0,失败时返回 -1。
示例:
close(sockfd);
socket 相关的结构体
在网络编程中,Socket 结构体是存储网络连接相关信息的核心数据结构之一。C/C++ 提供了一系列的结构体用于管理 IP 地址、端口、协议等信息,尤其是在 sockaddr 和 sockaddr_in 等结构体中,这些结构体帮助应用程序配置 Socket 的各种参数,便于网络通信的建立和管理。
sockaddr 结构体
sockaddr 是一个通用的地址结构体,用于不同的协议族(如 IPv4、IPv6、Unix 域套接字等)统一存储地址信息。
struct sockaddr {
sa_family_t sa_family; // 地址族,比如 AF_INET (IPv4) 或 AF_INET6 (IPv6)
char sa_data[14]; // 地址数据,通常包含 IP 地址和端口信息
};
sa_family
:指定地址族,例如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6。
sa_data
:存放 IP 地址和端口等信息,sa_data 并不直接存储 IPv4 或 IPv6 地址,而是提供一个通用的 14 字节数组,便于不同协议族的数据结构向其进行转换。
sockaddr_in 结构体(用于 IPv4)
IPv4 使用 sockaddr_in 结构体来管理网络地址。sockaddr_in 是 sockaddr 的一个特化结构,专门用于 IPv4,便于访问 IPv4 地址和端口。
struct sockaddr_in {
sa_family_t sin_family; // 地址族,IPv4 用 AF_INET
in_port_t sin_port; // 端口号(需要使用网络字节序)
struct in_addr sin_addr; // IP 地址
char sin_zero[8]; // 填充字段,不使用
};
struct in_addr {
uint32_t s_addr; // 存放 IP 地址(网络字节序)
};
sin_family
:地址族,IPv4 使用 AF_INET。
sin_port
:端口号,通常使用 htons() 转换为网络字节序。
sin_addr
:IP 地址,通常是 in_addr 结构体。
sin_zero
:填充字段,使得结构体大小与 sockaddr 保持一致。
在使用时,需要将 sockaddr_in 转换成通用的 sockaddr 类型,比如使用 bind() 或 connect() 函数时,需要强制转换。
sockaddr_in6 结构体(用于 IPv6)
sockaddr_in6 结构体用于存储 IPv6 地址信息。它是 sockaddr 的特化结构,用于支持更长的 IPv6 地址格式。
struct sockaddr_in6 {
sa_family_t sin6_family; // 地址族,IPv6 用 AF_INET6
in_port_t sin6_port; // 端口号(需要网络字节序)
uint32_t sin6_flowinfo; // 流信息(一般不常用)
struct in6_addr sin6_addr; // IPv6 地址
uint32_t sin6_scope_id; // 范围 ID(用于本地连接等)
};
struct in6_addr {
unsigned char s6_addr[16]; // 存储 128 位 IPv6 地址
};
sin6_family
:地址族,IPv6 使用 AF_INET6。
sin6_port
:端口号,和 sockaddr_in 一样,通常使用 htons() 转换为网络字节序。
sin6_flowinfo
:流信息字段,通常用于流控制,设置为 0 即可。
sin6_addr
:IP 地址,通常是 in6_addr 结构体。
sin6_scope_id
:范围 ID,用于指定区域范围,常用于本地连接。
IPv6 地址比 IPv4 更长,in6_addr 使用 16 字节数组 s6_addr 存储完整的 IPv6 地址。
使用 sockaddr 和 sockaddr_in/sockaddr_in6
当调用 bind()、connect() 等网络函数时,系统要求使用 sockaddr 结构体作为参数,而实际中一般用的是 sockaddr_in(IPv4)或 sockaddr_in6(IPv6)。所以需要将具体的结构体指针转换为通用的 sockaddr* 类型。
例如,使用 bind() 绑定一个 IPv4 地址:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8080); // 设置端口,使用网络字节序
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); // 设置 IP 地址
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
// 后续使用 listen()、accept() 等函数
}
其他常用 API
sendto 和 recvfrom:用于 UDP 数据报套接字的发送和接收,支持指定目标地址。
setsockopt 和 getsockopt:设置和获取套接字选项,例如设置超时时间、启用重用地址等。
select 和 poll:用于多路复用,可以同时监视多个套接字的事件(如读、写、异常)以实现非阻塞 I/O。
fcntl:可以设置套接字为非阻塞模式。
小结
这些 API 提供了 C/C++ 网络编程的基本操作,从创建和绑定套接字,到监听、接收连接,再到数据传输和关闭连接。这些函数组成了网络编程的核心接口,通过它们,开发者可以实现客户端-服务器、数据报通信、多路复用等复杂的网络通信模型。
以下是一个基于 TCP 的简单的客户端-服务器程序示例,展示了服务器和客户端的基本流程。
TCP、UDP的实现
服务器端代码示例(基于TCP)
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <cstring>
int main() {
int server_fd, client_fd;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建套接字
server_fd = socket(AF_INET, SOCK_STREAM, 0);
// 设置IP和端口
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有本地地址
address.sin_port = htons(8080); // 设置端口号
// 绑定套接字
bind(server_fd, (struct sockaddr *)&address, sizeof(address));
// 监听连接
listen(server_fd, 3);
std::cout << "Server listening on port 8080..." << std::endl;
// 接收客户端连接
client_fd = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
std::cout << "Client connected." << std::endl;
// 发送数据
const char *message = "Hello from server";
send(client_fd, message, strlen(message), 0);
// 关闭连接
close(client_fd);
close(server_fd);
return 0;
}
客户端代码示例(基于TCP)
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
char buffer[1024] = {0};
// 创建套接字
sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置服务器地址和端口
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);
// 连接服务器
connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
// 接收服务器消息
read(sock, buffer, 1024);
std::cout << "Message from server: " << buffer << std::endl;
// 关闭连接
close(sock);
return 0;
}
服务端代码示例(基于UDP)
UDP是无连接的协议,这意味着不需要像TCP那样监听连接、接受连接或管理连接状态。
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
char buffer[BUFFER_SIZE];
// 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(PORT);
// 绑定地址和端口
if (bind(sockfd, (const struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("Bind failed");
close(sockfd);
return -1;
}
std::cout << "Server listening on port " << PORT << std::endl;
while (true) {
socklen_t len = sizeof(client_addr);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&client_addr, &len);
buffer[n] = '\0'; // null-terminate the received string
std::cout << "Received message: " << buffer << std::endl;
// 回发接收到的消息
sendto(sockfd, buffer, n, 0, (struct sockaddr*)&client_addr, len);
}
close(sockfd);
return 0;
}
客户端代码示例(基于UDP)
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#define PORT 8080
#define SERVER_IP "127.0.0.1"
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 创建 UDP 套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("Socket creation failed");
return -1;
}
// 配置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);
// 输入消息并发送到服务器
std::cout << "Enter message: ";
std::cin.getline(buffer, BUFFER_SIZE);
sendto(sockfd, buffer, strlen(buffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 接收服务器的回应
socklen_t len = sizeof(server_addr);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&server_addr, &len);
buffer[n] = '\0'; // null-terminate the received string
std::cout << "Server response: " << buffer << std::endl;
close(sockfd);
return 0;
}
Socket编程中的常见问题和注意事项
- 资源管理:及时关闭Socket以释放系统资源,记得及时close不用的套接字。
- 错误处理:网络通信可能遇到各种错误,需检查每个操作的返回值。
- 多线程和异步编程:在高并发场景中,服务器通常使用多线程或异步编程来处理多个客户端请求。
- 缓冲区管理:数据传输过程中,合理的缓冲区设置可提高效率并避免数据丢失。