SOCKET 基础API

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 即关闭状态。
  • 绑定指定IP和端口
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;
}
  • 绑定本地Unix域套接字的地址
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 离开多播组
  • IPPROTO_IPV6(IPv6 协议层)
选项 类型 说明
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));
posted @ 2018-08-27 15:32  osbreak  阅读(1802)  评论(0)    收藏  举报