SOCKET 基础API
1.01 创建套接字
#include <sys/types.h>
#include <sys/socket.h>
domain:协议族
- AF_INET:IPv4
- AF_INET6:IPv6
type:套接字类型
- SOCK_STREAM:TCP(流式套接字)
- SOCK_DGRAM:UDP
protocol:通常设为 0(自动选择)
int socket(int domain, int type, int protocol);
bool Socket::Open( void ) {
if ( m_iFd > 0 ) {
return false;
}
if ( ( m_iFd = socket( m_iDomain, m_iType, m_iProtocol ) ) < 0 ) {
return false;
}
return true;
}
1.02 SOCKADDR_IN 地址结构体
SOCKADDR_IN socket_addr;
memset(&socket_addr, 0x0, sizeof(socket_addr)); //清零
socket_addr.sin_addr.S_un.S_addr = htonl(ADDR_ANY);//表示任何的ip过来连接都接受
socket_addr.sin_family = AF_INET;//使用IPV4地址
socket_addr.sin_port = htons(1234);//端口
1.03 用 TCP 进行网络通信,服务器端要经历几个状态,当调用 socket( ) 和 bind( ) 后,虽然套接字已经建立起来,但是其状态是 CLOSED 即关闭状态。
bool TcpSockServer::Bind( const std::string &ip, int32_t port ) {
if ( !IsValidSock() ) {
return false;
}
std::string strAddr = ip.empty() ? "0.0.0.0" : ip;
sockaddr_in inAddr = (struct sockaddr_in *)malloc( sizeof( struct sockaddr_in ) );
bzero( inAddr, sizeof( inAddr ) );
inAddr->sin_family = AF_INET;
inAddr->sin_addr.s_addr = utils::S2IP( strAddr, utils::BYTE_ORDER::NET_ORDER );
inAddr->sin_port = htons( port );
if ( bind( m_iFd, (struct sockaddr *)inAddr, sizeof( struct sockaddr_in ) ) != 0 ) {
return false;
}
m_bInit = true;
return m_bInit;
}
bool TcpSockServer::Bind( const std::string &path ) {
if ( !IsValidSock() ) {
return false;
}
struct sockaddr_un *unAddr = (struct sockaddr_un *)malloc( sizeof( struct sockaddr_un ) );
bzero( unAddr, sizeof( *unAddr ) );
unAddr->sun_family = AF_UNIX;
strncpy( unAddr->sun_path, path.c_str(), sizeof( unAddr->sun_path ) - 1 );
unlink( unAddr->sun_path );
if ( bind( m_iFd, (struct sockaddr *)unAddr, sizeof( struct sockaddr_un ) ) != 0 ) {
return false;
}
m_bInit = true;
return m_bInit;
}
1.04 listen监听申请的连接
backlog 用来描述 sockfd 的等待连接队列能够达到的最大值:
内核会在自己的进程空间里维护一个队列,这些连接请求就会被放入一个队列中,服务器进程会按照先来后到的顺序去处理这些连接请求,
这样的一个队列内核不可能让其任意大,所以必须有一个大小的上限,这个 backlog 参数告诉内核使用这个数值作为队列的上限。
而当一个客户端的连接请求到达并且该队列为满时,客户端可能会收到一个表示连接失败的错误,本次请求会被丢弃不作处理。
int listen(int sock, int backlog);
对于服务器端来说,套接字已经建立起来,但是其状态是 CLOSED 即关闭状态。要被动打开。
bool TcpSockServer::Listen( int32_t backlog ) {
if ( !m_bInit ) {
return false;
}
return listen( m_iFd, backlog ) == 0;
}
1.05 接受客户端连接(服务器专用)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept是一个阻塞函数,会进入到监听状态,等待客户端的连接请求。
当客户与服务器连接( connect )并且连接成功(三次握手)后 accept 则返回客户端的套接字。
TcpSockClientPtr TcpSockServer::Accept( sockaddr_in *sa ) {
socklen_t len = sizeof( *sa );
int32_t nFd = accept( m_iFd, (struct sockaddr *)sa, &len );
if ( nFd < 0 ) {
return nullptr;
}
TcpSockClientPtr cliSock = std::make_shared<TcpSockClient>();
cliSock->SetFd( nFd );
return cliSock;
}
1.06 连接服务器(客户端专用)
连接目的主机(服务器)的指定端口,以便和目的主机 ( 在 access 中等待 ) 进行通信
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);
bool TcpSockClient::Connect( const std::string &host, int32_t port ) {
DebugL << "socket: " << m_iFd << " Connecting!";
if ( !IsValidSock() ) {
FatalL << "socket: " << m_iFd << " valid, not connect!";
return false;
}
std::string strAddr = host;
if ( !utils::IsValidIPAddr( host ) ) {
strAddr = utils::DomainToIPAddr( host );
}
struct sockaddr_in svrAddr;
bzero( &svrAddr, sizeof( svrAddr ) );
svrAddr.sin_family = AF_INET;
svrAddr.sin_addr.s_addr = utils::S2IP( strAddr, utils::HY_BYTE_ORDER::NET_ORDER );
svrAddr.sin_port = htons( port );
return connect( m_iFd, (struct sockaddr *)&svrAddr, sizeof( svrAddr ) ) == 0;
}
1.07 发送和接收数据
| flags标志 |
说明 |
| MSG_CONFIRM |
确认地址有效,防止 ARP/邻居探测(仅 Linux,UDP) |
| MSG_DONTROUTE |
不使用路由表,直接发送到本地网络(绕过网关) |
| MSG_DONTWAIT |
非阻塞发送:如果不能立即发送,返回 EAGAIN 或 EWOULDBLOCK |
| MSG_EOR |
标记记录结束(SCTP/某些数据报协议,TCP 无效) |
| MSG_MORE |
表示还有更多数据要发送,延迟发送(配合 TCP_CORK 使用) |
| MSG_NOSIGNAL |
发送失败时不产生 SIGPIPE 信号(Linux 特有) |
| MSG_OOB |
发送带外数据(Out-of-Band Data),仅适用于支持 OOB 的协议(如 TCP) |
返回值:
- n:实际发送/接收的字节数
- 0:对端关闭连接(FIN)
- -1:出错
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
场景一、MSG_DONTWAIT : 非阻塞发送
即使 socket 是阻塞模式,也以非阻塞方式发送。如果内核发送缓冲区满,不等待,立即返回错误。
使用场景:
非阻塞 I/O 模型(如 select/poll/epoll),避免线程阻塞。
ssize_t sent = send(sockfd, buf, len, MSG_DONTWAIT);
if (sent == -1) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 缓冲区满,稍后重试
} else {
// 真正的错误
perror("send");
}
}
场景二、MSG_NOSIGNAL : 防止 SIGPIPE 信号(Linux 特有)
作用:当对一个已关闭的 socket 调用 send() 时,不会触发 SIGPIPE 信号,而是返回 -1 并设置 errno = EPIPE。
使用场景:
多线程程序中避免信号处理复杂性,希望通过返回值判断错误,而不是信号
ssize_t sent = send(sockfd, buf, len, MSG_NOSIGNAL);
if (sent == -1 && errno == EPIPE) {
// 连接已关闭,安全处理
close(sockfd);
}
1.08 辅助函数功能
| 名称 |
说明 |
| htons() / htonl() |
主机字节序 → 网络字节序(short/long) |
| ntohs() / ntohl() |
网络字节序 → 主机字节序 |
| inet_addr() |
字符串 IP → 32 位整数(已过时) |
| inet_aton() |
字符串 IP → struct in_addr |
| inet_ntoa() |
struct in_addr → 字符串 IP(IPv4) |
| inet_pton() |
字符串 IP → 网络字节序(支持 IPv4/IPv6) |
| inet_ntop() |
网络字节序 → 字符串 IP(支持 IPv4/IPv6) |
int inet_pton(int af, const char *src, void *dst);
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
将点分文本的IP地址转换为二进制网络字节序的IP地址,而且inet_pton和inet_ntop这2个函数能够处理ipv4和ipv6。
struct in_addr addr;
inet_pton(AF_INET, "192.168.1.222", &addr);
printf("ip addr: 0x%x\n", addr.s_addr);
struct in_addr addr;
addr.s_addr = 0xde01a8c0;
char buf[20] = {0};
inet_ntop(AF_INET, &addr, buf, sizeof(buf));
printf("ip addr: %s\n", buf);
主机字节序转网络字节序
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
网络字节序转主机字节序
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
1.09 close、shutdown关闭套接字
close会将套接字描述符的引用计数减1,如果引用计数仍大于0,则不会引起TCP的四次挥手终止序列。
int close(int fd);
int shutdown(int sockfd, int how);
SHUT_RD:关闭读端
SHUT_WR:关闭写端
SHUT_RDWR:同时关闭读写端
shutdown(sockfd, SHUT_WR); // 告诉对方“我发完了”
// 还可以继续 recv()
2.01 UDP 发送函数
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
2.02 UDP 接收函数
写一个长度为0的数据报是可行的。在UDP情况下,这会形成一个只包含一个IP首部(对于IPv4通常为20字节,对于IPv6通常为40字节)和一个8字节UDP首部而没有数据的IP数据报。
这也意味着对于数据报协议,recvfrom返回0值是可接受的。
它并不像TCP套接字上read返回0值那样表示对端已关闭连接。
既然UDP是无连接的,因此也就没有诸如关闭一个UDP连接之类的事情。
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
3.01 Windows控制套接字的 I/O 模式
ioctlsocket函数主要是用来设置或取消非阻塞套接字的。
将套接字设置为非阻塞式套接字后,connect、send和recv等函数将变成非阻塞式的,调用后会立即返回,执行的操作结果成功与否,需要通过后续代码去判断。
unsigned long unblock = 1;
int ret = ioctlsocket(tSock, FIONBIO, (unsigned long *)&unblock);
3.02 Linux控制套接字的 I/O 模式
- 配置 socket 选项的核心系统调用。它允许你在不同协议层(如 socket 层、TCP 层、IP 层)设置各种参数,以控制 socket 的行为。
int setsockopt( int sockfd, int level, int optname, const void* optval, socklen_t optlen);
iInt getsockopt(int sockfd, int level, int optname, void* optval, socklen_t optlen);
int level:
- SOL_SOCKET(通用 socket 层) 这是最常用的层级,适用于所有类型的 socket。
| 选项 |
类型 |
说明 |
| SO_REUSEADDR |
int |
允许重用本地地址(端口),避免 Address already in use 错误 |
| SO_REUSEPORT |
int |
Linux 特有,允许多个 socket 绑定同一端口(用于负载均衡) |
| SO_KEEPALIVE |
int |
启用 TCP 连接保活机制(心跳检测) |
| SO_LINGER |
struct linger |
控制 close() 时是否等待未发送数据 |
| SO_SNDBUF |
int |
设置发送缓冲区大小 |
| SO_RCVBUF |
int |
设置接收缓冲区大小 |
| SO_SNDLOWAT |
int |
发送低水位标记(触发可写事件的最小数据量) |
| SO_RCVLOWAT |
int |
接收低水位标记 |
| SO_SNDTIMEO |
struct timeval |
发送超时时间 |
| SO_RCVTIMEO |
struct timeval |
接收超时时间 |
| SO_BROADCAST |
int |
允许发送广播数据(UDP) |
| SO_OOBINLINE |
int |
将带外数据(OOB)放入正常数据流 |
| SO_ERROR |
int |
获取 socket 错误状态(只读) |
| SO_TYPE |
int |
获取 socket 类型(如 SOCK_STREAM,只读) |
- IPPROTO_TCP(TCP 协议层)仅适用于 TCP socket。
| 选项 |
类型 |
说明 |
| TCP_NODELAY |
int |
禁用 Nagle 算法,立即发送小数据包(降低延迟) |
| TCP_KEEPIDLE |
int |
TCP 空闲多久后开始发送 keepalive 探测(秒) |
| TCP_KEEPINTVL |
int |
keepalive 探测间隔时间(秒) |
| TCP_KEEPCNT |
int |
最大 keepalive 探测次数 |
| TCP_MAXSEG |
int |
设置 TCP 最大段大小(MSS) |
| TCP_COR |
int |
Linux 特有,启用 cork 模式(延迟发送,合并小包) |
| TCP_QUICKACK |
int |
Linux 特有,禁用延迟确认(提高响应速度) |
- IPPROTO_IP(IP 协议层)适用于 IPv4 socket。
| 选项 |
类型 |
说明 |
| IP_TOS |
int |
设置 IP 数据包的服务类型(Type of Service) |
| IP_TTL |
int |
设置 IP 数据包的生存时间(Time To Live) |
| IP_HDRINCL |
int |
原始 socket 中包含 IP 头(用于自定义 IP 包) |
| IP_MULTICAST_TTL |
int |
设置多播包的 TTL |
| IP_MULTICAST_LOOP |
int |
是否允许多播包回环 |
| IP_ADD_MEMBERSHIP |
struct |
ip_mreq 加入多播组 |
| IP_DROP_MEMBERSHIP |
struct |
ip_mreq 离开多播组 |
| 选项 |
类型 |
说明 |
| IPV6_V6ONLY |
int |
是否只绑定 IPv6 地址(避免与 IPv4 冲突) |
| IPV6_TCLASS |
int |
设置流量类别(Traffic Class) |
| IPV6_UNICAST_HOPS |
int |
设置单播跳数(类似 TTL) |
| IPV6_MULTICAST_HOPS |
int |
多播跳数 |
| IPV6_JOIN_GROUP / IPV6_LEAVE_GROUP |
struct ipv6_mreq |
加入/离开 IPv6 多播组 |
| 层级 |
用途 |
| SOL_RAW |
原始 socket 选项 |
| SOL_PACKET |
数据链路层(如抓包) |
| SOL_SOCKET + SO_DEBUG |
启用 socket 调试(依赖内核支持) |
// 设置发送缓冲区大小
int optVal = 2*1024*1024; // 2MB
setsockopt(tSock, SOL_SOCKET, SO_SNDBUF, (char *)&optVal, sizeof(optVal));
// 设置接收缓冲区大小
int optVal = 2*1024*1024; // 2MB
setsockopt(tSock, SOL_SOCKET, SO_RCVBUF, (char *)&optVal, sizeof(optVal));
// 获取实际发送长度
int sent_bytes;
getsockopt(tSock, IPPROTO_TCP, TCP_INFO, &sent_bytes, sizeof(optVal);
// 设置端口复用
int32_t iSetReuse = 1;
setsockopt( m_iFd, SOL_SOCKET, SO_REUSEADDR, &iSetReuse, sizeof( iSetReuse )
// 启用 TCP 心跳(Linux)
int keepalive = 1;
int idle = 60;
int interval = 10;
int count = 3;
setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, sizeof(keepalive));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval));
setsockopt(sockfd, IPPROTO_TCP, TCP_KEEPCNT, &count, sizeof(count));
// 设置接收超时
struct timeval tv = {5, 0}; // 5 秒
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
/*默认行为(启用Nagle 算法):TCP 会尝试将多个小数据包合并成一个大的 TCP 段发送,以减少网络中小包的数量。
但前提是:必须等前一个未确认的小包被 ACK 后,才能发送下一个。
这会导致延迟增加(latency),尤其是在频繁发送小数据的应用中。*/
/*禁用 Nagle 算法(降低延迟),使得小数据包可以立即发送,不等待后续数据合并。
小数据包(如几个字节)会立即发送,不等待 ACK。
降低延迟,提高响应速度。
可能增加网络中小包数量(轻微带宽浪费)。*/
int nodelay = 1;
setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
建议开启:对于大多数现代应用(尤其是微服务、API、数据库客户端),建议默认启用 TCP_NODELAY。
两端都设:客户端和服务端都设置效果最佳。
结合 SO_SNDBUF 调优:适当增大发送缓冲区,减少阻塞。
4.01 select多路复用
#include <sys/select.h>
struct timeval{
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
void FD_ZERO(fd_set *fdset); // 清除fdset的所有位
void FD_SET(int fd,fd_set *fdset); // 打开fdset中的fd位
void FD_CLR(int fd,fd_set *fdset); // 清除fdset中的fd位
int FD_ISSET(int fd,fd_set *fdset); // 检查fdset中的fd位是否置位
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// 定义集合
fd_set fdRead;
fd_set fdWrite;
fd_set fdExp;
// 清理集合
FD_ZERO(&fdRead);
FD_ZERO(&fdWrite);
FD_ZERO(&fdExp);
// 将描述符(socket)加入集合
FD_SET(_sock, &fdRead);
FD_SET(_sock, &fdWrite);
FD_SET(_sock, &fdExp);
SOCKET maxSock = _sock;
// select 函数不设置 timeval 时,select是阻塞的,直到有数据。
// select 函数设置 timeval 时,超时即返回、select是非阻塞的。
timeval t = { 1,0 };
int ret = select(maxSock + 1, &fdRead, &fdWrite, &fdExp, &t);
// 判断描述符(socket)是否在集合中
if (FD_ISSET(_sock, &fdRead)){}
5.01 ioctl()函数简介
ioctl()函数是一个在 Unix 和 Linux 系统中广泛使用的系统调用,用于对文件描述符(通常是设备文件)执行设备或协议特定的控制操作,这些操作无法通过标准的 read()、write() 等系统调用完成。
一、网络接口管理(Network Interface Control)
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <arpa/inet.h>
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct ifreq ifr;
// 设置接口名
strcpy(ifr.ifr_name, "eth0");
// 获取MAC地址
ioctl(sockfd, SIOCGIFHWADDR, &ifr);
unsigned char *mac = (unsigned char *)ifr.ifr_hwaddr.sa_data;
printf("MAC: %02x:%02x:%02x:%02x:%02x:%02x\n", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
//获取IP地址
ioctl(sockfd, SIOCGIFADDR, &ifr);
struct sockaddr_in *ip = (struct sockaddr_in *)&ifr.ifr_addr;
printf("IP: %s\n", inet_ntoa(ip->sin_addr));