一.select代码讲解
关于这部分代码,我会重点讲解套接字还有select服务端的内容,其余代码不做讲解!关于下面参数介绍的时候:in(输入参数,我们交给OS)out(输出参数,OS交给我们)in/out(输入/输出函数,我们交给OS,然后OS在交给我们)
二.socket代码
2.1 基础部分
cpp
// 定义套接字操作的致命错误码(2 开始,避开系统保留值0表示成功,1表示通用错误)
enum
{
SocketErr = 2, // 创建套接字失败
BindErr, // 绑定地址失败
ListenErr, // 监听失败
};
// 监听队列的最大长度(等待连接的客户端数量上限)
const int backlog = 10;
Sock()
: sockfd_(-1) // 构造函数:初始化文件描述符为无效值
{}
~Sock()
{
// 析构函数:自动关闭有效套接字,防止资源泄漏
if (sockfd_ >= 0)//避免无效关闭无效的文件描述符
{
close(sockfd_); // 关闭底层文件描述符
sockfd_ = -1; // 重置为无效值,避免重复关闭
}
}
关于这部分知识,重要的是enum这个枚举为什么要从2开始,注释有做解释,至于其他内容则无需讲解,看注释即可!
2.2 socket常见API
2.2.1 Socket
cpp
void Socket()
{ // 创建 TCP 套接字并启用地址/端口复用
sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建 IPv4 流式套接字
if (sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);//
exit(SocketErr); // 创建失败,程序终止
}
int opt = 1;
// 启用 SO_REUSEADDR,允许快速重启服务
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
要点:
- 明白socket函数的意思
cpp// 创建一个套接字,用于网络通信 // 返回一个文件描述符,代表该网络端点 #include <sys/socket.h> int socket(int domain, int type, int protocol); // 参数: // domain - [in] 协议族(如 AF_INET 表示 IPv4,AF_INET6 表示 IPv6) // type - [in] 套接字类型(如 SOCK_STREAM 表示 TCP,SOCK_DGRAM 表示 UDP) // protocol - [in] 指定具体协议(通常设为 0,表示使用默认协议) // 返回值: // 成功:返回非负整数(新的套接字文件描述符) // 失败:返回 -1,并设置 errno
- 明白setsockopt函数的意思
cpp// 设置 socket 选项(控制底层网络行为) // 常用于调整缓冲区、超时、重用地址等 #include <sys/socket.h> int setsockopt( int sockfd, int level, int optname, const void *optval, socklen_t optlen ); // 参数说明: // sockfd - socket 文件描述符 // level - 选项层级: // SOL_SOCKET → 通用 socket 层 // IPPROTO_TCP → TCP 层 // IPPROTO_IP → IP 层 // optname - 具体选项名(如 SO_REUSEADDR) // optval - 指向选项值的指针(通常为 int 或 struct) // optlen - optval 的大小(字节数) // 返回值: // 成功:0 // 失败:-1,设置 errno // 示例: // 1. 允许端口重用(解决 "Address already in use") int reuse = 1; setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
2.2.2 Bind
cpp
// 绑定到本地任意 IP 的指定端口
void Bind(uint16_t port)
{
struct sockaddr_in local; // 本地地址结构
memset(&local, 0, sizeof(local)); // 清零结构体内存
local.sin_family = AF_INET; // 协议族设为 IPv4
local.sin_port = htons(port); // 主机字节序端口转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr); // 绑定失败,程序终止
}
}
要点:
- 明白Bind函数的意思
cpp// 将套接字绑定到指定的本地地址和端口 // 服务器通常需要显式 bind 客户端一般由系统自动分配 #include <sys/socket.h> int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 参数: // sockfd - [in] 套接字文件描述符(由 socket() 创建) // addr - [in] 指向本地地址结构的指针(如 struct sockaddr_in) // addrlen - [in] addr 结构体的字节大小(如 sizeof(struct sockaddr_in)) // 返回值: // 成功:返回 0 // 失败:返回 -1 并设置 errno
- 了解sockaddr_in结构体
cpp// IPv4 套接字地址结构体(用于 bind, connect, accept 等) // 定义在 <netinet/in.h> #include <netinet/in.h> struct sockaddr_in { sa_family_t sin_family; // 地址族:AF_INET(必须) in_port_t sin_port; // 端口号(网络字节序,16 位) struct in_addr sin_addr; // IPv4 地址(网络字节序,32 位) char sin_zero[8]; // 填充字段(全置 0,使结构体与 sockaddr 大小一致) }; // 成员说明: // sin_family - 固定为 AF_INET(表示 IPv4) // sin_port - 用 htons() 转换主机字节序端口到网络字节序 // 例: sin_port = htons(8080); // sin_addr - 用 inet_addr() 或 inet_pton() 设置 IP 地址 // 例: sin_addr.s_addr = inet_addr("192.168.1.1"); // 或 inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr); // sin_zero - 必须清零(通常用 memset 或直接赋值)
- in_addr也是一个结构体
cpp// 表示 IPv4 地址(32 位无符号整数,网络字节序) // 定义在 <netinet/in.h> #include <netinet/in.h> struct in_addr { in_addr_t s_addr; // 通常为 uint32_t,存储 IPv4 地址 }; // 关键点: // • s_addr 的值是 **网络字节序(大端)** // • 不能直接赋主机整数值(需用转换函数)
2.2.3 Listen
cpp
// 将套接字设为监听状态
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr); // 监听失败,程序终止
}
}
要点:
- 明白listen的函数
cpp// 将套接字设为监听状态 准备接收客户端连接请求(仅用于 TCP) // 通常在 bind 之后调用 #include <sys/socket.h> int listen(int sockfd, int backlog); // 参数: // sockfd - [in] 套接字文件描述符(必须是已 bind 的 TCP 套接字) // backlog - [in] 连接请求队列的最大长度(内核会根据系统限制调整) // 返回值: // 成功:返回 0 // 失败:返回 -1 并设置 errno
2.2.4 Accept
cpp
// 接受新连接,并输出客户端地址信息
int Accept(std::string *clientip, uint16_t *clientport)
{
// 存储客户端地址
struct sockaddr_in peer;
// 地址结构大小
socklen_t len = sizeof(peer);
// 阻塞等待连接
int newfd = accept(sockfd_, (struct sockaddr *)&peer, &len);
if (newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1; // 接受失败,返回无效 fd
}
char ipstr[64];
// 将二进制 IPv4 地址安全转换为可读字符串
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr; // 输出客户端 IP 字符串
*clientport = ntohs(peer.sin_port); // 网络字节序端口转主机字节序
return newfd; // 返回新连接的文件描述符
}
要点:
- 明白accept函数
cpp// 从监听套接字的连接请求队列中取出一个新连接 // 创建一个新的套接字用于与客户端通信(原监听套接字继续监听) #include <sys/socket.h> int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 参数: // sockfd - [in] 监听套接字的文件描述符(必须已调用 listen()) // addr - [out] 指向 sockaddr 结构的指针,用于接收客户端地址(可为 NULL) // addrlen - [in/out] // 输入:addr 缓冲区的字节大小 // 输出:实际写入的客户端地址结构体长度 // 返回值: // 成功:返回新的已连接套接字描述符 // 失败:返回 -1 并设置 errno
- 明白inet_ntop函数
cpp// 将网络字节序的 IPv4 或 IPv6 地址(二进制形式) // 转换为标准可读字符串(如 "192.168.1.1" 或 "2001:db8::1") #include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 参数: // af - [in] 地址族: // AF_INET → src 指向 struct in_addr(IPv4) // AF_INET6 → src 指向 struct in6_addr(IPv6) // src - [in] 指向包含网络字节序 IP 地址的结构体的指针 // dst - [out] 输出缓冲区,用于存放转换后的字符串 // size - [in] dst 缓冲区的大小(字节数) // 返回值: // 成功:返回 dst 指针 // 失败:返回 NULL,并设置 errno(如 ENOSPC 表示缓冲区太小)
2.2.5 Connect
cpp
// 主动连接到指定服务器
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer; // 服务器地址结构
memset(&peer, 0, sizeof(peer)); // 初始化内存
peer.sin_family = AF_INET; // IPv4 协议
peer.sin_port = htons(port); // 端口转网络字节序
// 将点分十进制 IP 字符串转换为二进制网络字节序
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false; // 连接失败
}
return true; // 连接成功
}
要点:
- 明白connect函数
cpp// 主动发起连接到指定服务器地址(用于 TCP 客户端) // 建立与远程主机的连接后即可进行数据收发 #include <sys/socket.h> int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // 参数: // sockfd - [in] 套接字文件描述符(由 socket() 创建,通常为 SOCK_STREAM) // addr - [in] 指向目标服务器地址结构的指针(如 struct sockaddr_in) // addrlen - [in] addr 结构体的字节大小(如 sizeof(struct sockaddr_in)) // 返回值: // 成功:返回 0 // 失败:返回 -1 并设置 errno
- 明白inet_ntop函数
cpp// 将网络字节序的 IPv4 或 IPv6 地址(二进制)转换为标准可读字符串格式 #include <arpa/inet.h> const char *inet_ntop(int af, const void *src, char *dst, socklen_t size); // 参数: // af - [in] 地址族: // AF_INET → src 指向 struct in_addr(IPv4) // AF_INET6 → src 指向 struct in6_addr(IPv6) // src - [in] 指向包含 IP 地址(网络字节序)的结构体的指针 // dst - [out] 用于存储输出字符串的缓冲区 // size - [in] dst 缓冲区的大小(以字节为单位) // 返回值: // 成功:返回 dst 指针 // 失败:返回 NULL,并设置 errno(常见错误:ENOSPC --- 缓冲区太小)
三.完整代码
cpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cerrno>
#include "Log.hpp"
// 定义套接字操作的致命错误码(从 2 开始,避开系统保留值0表示成功,1表示通用错误)
enum
{
SocketErr = 2, // 创建套接字失败
BindErr, // 绑定地址失败
ListenErr, // 监听失败
};
// 监听队列的最大长度(等待连接的客户端数量上限)
const int backlog = 10;
class Sock
{
public:
Sock()
: sockfd_(-1) // 构造函数:初始化文件描述符为无效值
{}
~Sock()
{
// 析构函数:自动关闭有效套接字,防止资源泄漏
if (sockfd_ >= 0)//避免无效关闭无效的文件描述符
{
close(sockfd_); // 关闭底层文件描述符
sockfd_ = -1; // 重置为无效值,避免重复关闭
}
}
void Socket()
{ // 创建 TCP 套接字并启用地址/端口复用
sockfd_ = socket(AF_INET, SOCK_STREAM, 0); // 创建 IPv4 流式套接字
if (sockfd_ < 0)
{
lg(Fatal, "socket error, %s: %d", strerror(errno), errno);//
exit(SocketErr); // 创建失败,程序终止
}
int opt = 1;
// 启用 SO_REUSEADDR,允许快速重启服务
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
// 绑定到本地任意 IP 的指定端口
void Bind(uint16_t port)
{
struct sockaddr_in local; // 本地地址结构
memset(&local, 0, sizeof(local)); // 清零结构体内存
local.sin_family = AF_INET; // 协议族设为 IPv4
local.sin_port = htons(port); // 主机字节序端口转网络字节序
local.sin_addr.s_addr = INADDR_ANY; // 绑定所有本地网络接口
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr); // 绑定失败,程序终止
}
}
// 将套接字设为监听状态
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr); // 监听失败,程序终止
}
}
// 接受新连接,并输出客户端地址信息
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer; // 存储客户端地址
socklen_t len = sizeof(peer); // 地址结构大小
int newfd = accept(sockfd_, (struct sockaddr *)&peer, &len); // 阻塞等待连接
if (newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1; // 接受失败,返回无效 fd
}
char ipstr[64];
// 将二进制 IPv4 地址安全转换为可读字符串
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr; // 输出客户端 IP 字符串
*clientport = ntohs(peer.sin_port); // 网络字节序端口转主机字节序
return newfd; // 返回新连接的文件描述符
}
// 主动连接到指定服务器
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer; // 服务器地址结构
memset(&peer, 0, sizeof(peer)); // 初始化内存
peer.sin_family = AF_INET; // IPv4 协议
peer.sin_port = htons(port); // 端口转网络字节序
// 将点分十进制 IP 字符串转换为二进制网络字节序
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr *)&peer, sizeof(peer));
if (n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false; // 连接失败
}
return true; // 连接成功
}
// 显式关闭套接字(通常由析构函数自动完成)
void Close()
{
if (sockfd_ >= 0)
{
close(sockfd_); // 释放系统资源
sockfd_ = -1; // 标记为无效
}
}
// 获取底层文件描述符(只读接口)
int Fd() const
{
return sockfd_;
}
private:
int sockfd_; // TCP 套接字文件描述符,-1 表示无效
};
关于今天的内容就是讲解select中的socket如何写,希望大家能好好学习,学习这个也是复习我们之前讲的套接字知识!