基本套接字

大小端

大端序就是高位字节储存到低地址处,小端则相反。由于 PC 大多使用小端序,因此 小端序也被称为主机字节序 。但是在进行网络传输时统一使用大端序,因此 大端序也被称为网络字节序

Java 虚拟机采用大端序

Linux 提供了一组函数用来在网络字节序和主机字节序之间进行转换:

uint32_t ntohl (uint32_t __netlong);
uint16_t ntohs (uint16_t __netshort);
uint32_t htonl (uint32_t __hostlong);
uint16_t htons (uint16_t __hostshort);

函数的名字含义为: host to network long、host to network short 等。

长整型一般用来转换 IP 地址,短整型一般用来转换端口号

地址格式

一个完整的地址格式由 IP 地址加上端口号组成,这种形式唯一地标示了计算机网络中的一个进程。因此套接字实际上就是网络中的 IPC。

逻辑上,Unix 中表示地址格式的结构体为:

Diagram

这只是逻辑上的关系,实际上由于 C 语言的限制,sockaddr_int 和 sockaddr_in6 需要使用强制类型转换转为 sockaddr。

in_addr_t 和 in6_addr_t 也是结构体,其内容物是一个名为 in_addr_t 和 in6_addr_t 的数据成员。

Unix 还提供了几组函数用来在二进制数据和点分十进制之间进行转换:

const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);
 int inet_pton(int af, const char *src, void *dst);

这两个函数都同时支持 IPv4 和 IPv6

套接字描述符

套接字描述符代表了通信端点,可视为文件描述符的特化。因此,很多用与文件描述符的函数也可用于套接字描述符,套接字的定义为:

int socket(int domain, int type, int protocol);

参数 domain 用来确定通信特性,其参数为:

参数描述

AF_INET

IPv4 因特网域

AF_INET6

IPv6 因特网域

AF_UNIX

UNIX 域

AF_UNSPEC

未指定

AF 的含义是 Address Family。根据实现,其参数可能有很多种,但是常用的就是这几种

type 确定了套接字的类型,其值为:

类型描述

SOCK_DGRAM

UDP

SOCK_RAW

IP 协议的数据报接口

SOCK_SEQPACKET

基于报文的 TCP

SOCK_STREAM

TCP

绑定地址

在创建套接字文件描述符后,还需要将其与特定的地址关联起来(类似于将 fd 转换为 FILE*)。对于客户端而言,可以让操作系统选择一个任意的端口,但是服务器需要明确指出需要绑定的地址:

int bind(int sockfd, const sockaddr *addr, socklen_t addrlen);

监听地址

对于服务器而言,绑定地址后还需要对端口进行持续监听是否有连接进入,并决定是否接受连接:

int listen(int sockfd, int backlog);

backlog 用来提示连接队列的大小。其硬上限被储存到 SOMAXCONN 中

  • 如果在创建套接字后创建子进程,则子进程也会监听相关的套接字。父进程退出后子进程还会监听套接字

  • 如果在执行程序的时候发现端口被占用,则可以使用 sudo lsof -i:9322 查看占用情况

建立连接

对于面向连接的网络服务而言,在发送数据前必须先进行连接以建立一个可靠的网络链路

int connect(int sockfd, const sockaddr *addr, socklen_t addrlen);

要想连接成功,必须:

  • 目标服务是开启的

  • 服务器的等待队列未满

  • 服务器接受连接

在连接过程中,可能会产生一些瞬时错误,应用程序必须具备处理它的能力

对于基于 BSD 的套接字实现(FreeBSD/MacOS)中,connect 一旦失败就没法继续用了。因此可移植的用法是每次 connect 失败后都关闭套接字并创建一个新的来用

需要区分连接套接字和监听套接字。在并发服务器中,常见的情况是父进程持有监听套接字,然后每当一个新的连接进入,都 fork 一个子进程来处理连接套接字。连接套接字和监听套接字拥有相同的端口号。系统根据数据的来源 IP 和端口决定转发给哪一个进程。

接受连接

一旦服务器开始监听地址,并有连接进入,服务器就能够选择是否接受连接:

int accept(int sockfd, sockaddr * addr, socklen_t * addrlen);

accept 的返回值是代表客户端的套接字描述符。参数 addr 是输出参数,用来获取服务端的信息,如果不管兴趣可以设为 NULL。addrlen 代表了 addr 的长度

创建套接字

socker 中代表套接字的是结构体 sockaddr,但是为了使用方便,从中又特化出 sockaddr_in 用于 IPv4、sockaddr_in6 用于 IPv6:

struct sockaddr_in{
   sa_family_t sin_family; // 地址族
   u_int16_t sin_port; // 端口号
   struct in_addr sin_addr; // IPv4 地址
}

struct in_addr{
    u_int32_t s_addr;
}

其中,地址族为:

协议族地址族含义

PF_INET

AF_INET

IPv4

PF_INET6

AF_INET6

IPv6

地址族和协议族的值是相等的,因此可以相互替换

下面的函数可用于将点分十进制的地址转换为网络字节序整数表示的 IP 地址:

#include <arpa/inet.h>
int inet_pton(int af, const char* src, void* dst);

inet_pton 将 src 表示的 IP 地址转换到网络字节序,并将其储存到 dst 中。af 用来指定地址族

而函数 inet_ntop 则进行相反的转换

使用 listen 可以用来监听套接字。listen 有两个参数:第一个参数用来指定 fd,第二个参数用来指定最大的监听队列。当队列的长度超过指定的长度时,新的连接会被拒绝,同时客户端得到 ECONNREFUSED 信息

#include <spdlog/spdlog.h>

#include <thread>

extern "C" {
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
}

int main() {
   auto fd = socket(PF_INET, SOCK_STREAM, 0);
   if(fd == -1) {
      spdlog::critical("套接字创建失败");
      exit(-1);
   }
   sockaddr_in address;
   bzero(&address, sizeof(address));
   address.sin_family = AF_INET;
   inet_pton(AF_INET, "127.0.0.1", &address.sin_addr);
   address.sin_port = htons(9020);
   if(bind(fd, (sockaddr*)&address, sizeof(address)) == -1) {
      spdlog::critical("绑定端口失败");
      exit(-1);
   }
   if(listen(fd, 5) == -1) {
      spdlog::critical("监听失败");
      exit(-1);
   }
   while(true) { std::this_thread::yield(); }
   return 0;
}

接受连接

我们现在可以接收来自客户端的连接:

while(true) {
   sockaddr_in client;
   socklen_t   clientLen = sizeof(client);
   int         connfd    = accept(fd, (sockaddr*)&client, &clientLen);
   if(connfd == -1) {
      spdlog::error("连接客户端套接字失败");
      continue;
   }
   char* remote     = new char[INET_ADDRSTRLEN];
   auto  remotePtr  = std::make_shared<char*>(remote);
   auto  clientIp   = inet_ntop(AF_INET, &client.sin_addr, remote, INET_ADDRSTRLEN);
   auto  clientPort = ntohs(client.sin_port);
   std::cout << clientIp << ":" << clientPort << std::endl;
   close(connfd);
   std::this_thread::yield();
}

Linux 每次 fork 都会导致父进程中打开的套接字的引用计数加一,因此必须在父子进程中都调用 close 才能将连接关闭。如果需要立即关闭,则可以使用 shutdown 系统调用:

#include <sys/socket.h>
int shutdown(int sockfd, int howto);

第二个参数指定了 shutdown 的行为:

可选值含义

SHUT_RD

关闭读端

SHUT_WR

关闭写端

SHUT_RDWR

关闭写端和读端

接收和发送数据

将数据接收再发送回去,就完成了一个 echo 服务器:

while(true) {
   sockaddr_in client;
   socklen_t   clientLen = sizeof(client);
   int         connfd    = accept(fd, (sockaddr*)&client, &clientLen);
   if(connfd == -1) {
      spdlog::error("连接客户端套接字失败");
      continue;
   }
   char buffer[1024];
   memset(buffer, '\0', 1024);
   int recvLen = recv(connfd, buffer, 1023, 0);
   std::cout << buffer << std::endl;
   send(connfd, buffer, recvLen, 0);
   close(connfd);
   std::this_thread::yield();
}

如果向一个关闭的套接字两次调用 send 函数时,会产生 SIGPIPE 信号。一般来说这种套接字的出现是客户端未调用 close 关闭套接字引起的

这种情况下有两种解决方案:

  • 调用 signal(SIGPIPE,SIG_IGN) 函数忽略 SIGPIPE 信号

  • 调用 send 函数时,指定 flag = MSG_NOSIGNAL,即 send(clientSocket, buf, len, MSG_NOSIGNAL);

客户端

客户端与服务器前半部分相同,唯一不同的是客户端无需绑定到端口上,直接 connect 即可:

int main() {
   sockaddr_in serverAddress;
   bzero(&serverAddress, sizeof(serverAddress));
   serverAddress.sin_family = AF_INET;
   inet_pton(AF_INET, "127.0.0.1", &serverAddress.sin_addr);
   serverAddress.sin_port = htons(9020);
   int fd                 = socket(PF_INET, SOCK_STREAM, 0);
   if(fd == -1) {
      spdlog::critical("套接字创建失败");
      exit(-1);
   }
   if(connect(fd, (sockaddr*)&serverAddress, sizeof(serverAddress)) < 0){
      spdlog::critical("连接失败");
      exit(-1);
            }
   const char* oobData = "abc\n";
   const char* normalData = "123\b";
   send(fd, normalData, strlen(normalData), 0);
   send(fd, oobData, strlen(oobData), MSG_OOB);
   send(fd, normalData, strlen(normalData), 0);
   close(fd);
   return 0;
}

发送缓冲区

IPv4 要求的最小 MTU 是 68 字节,而最大字节一般为 1500 字节。在转发过程中,如果 IP 数据报的大小超过了对应链路的 MTU,数据报会被分片,转发过程中不会进行重组操作。如果数据包的 DF 位被设置,则转发过程中不会进行分片,如果分片超过了目的路径 MTU,将会得到报错消息。在 IPv6 环境下,DF 位是默认开启的,且不可关闭

当路由器是 IPv6 的数据包的产生端时,可以对数据包进行分片,但是当它转发数据包时不进行分片

当某个进程调用 write 写入数据的时候,数据首先将数据从应用程序缓冲区拷贝到 TCP 发送缓冲区,如果发送缓冲区容量不足,进程将被阻塞,write 系统调用不会返回,直至所有数据被拷贝到发送缓冲区

当 write 返回时,只是表明需要发送的数据已经被拷贝到了发送缓冲区,但是不一定已经送到了对方

由于 write 的这种特性,让很多人认为 write/sendto 是非阻塞的,显然这种想法是错误的。

与 TCP 不同的是,UDP 没有发送缓冲区,因为 UDP 是不可靠协议,无需存放数据副本,当 UDP 发送数据的时候,数据包沿协议栈向下被复制到内核缓冲区中,然后返回。

如果 UDP 套接字没有空间存放需要发送的数据,则 sendto 会被阻塞至空间可用

UDP

UDP 是面向 报文 的无连接协议,其流程如下:

image

bind 函数是可选的,更准确来讲 bind 和 connect 是一个重载函数,其在 UDP 下的语义为:

  • bind 用来固定套接字发送方的地址

  • connect 用来确定接收方的地址

和 TCP 不同的是,UDP 是 报文 协议,这意味着:

  • 每次读取 UDP 套接字都会读取一个报文,如果本次数据未读完,剩余数据被丢弃

  • UDP 对整个报文计算校验码,因此数据要么一个不到达,要么全部到达

  • 如果数据太大,就会在 IP 层进行分片,然后在目的计算机上进行重组

  • 如果服务端未运行,则目的主机返回一个 ICMP 端口不可达信息,但是这个错误只有 connect 的套接字才能看到

其中第一个情况被称为 报文截断 。如果使用 recvmsg 读取消息,则 MSG_TRUNC 标志会被置一。

要解决这个办法,可以使用 FIONREAD 来查看 fd 中可读的消息的大小。

ulong bytesToRecv = 0;
ioctl(fd, FIONREAD, &bytesToRecv);

connect

一个未调用 connect 的套接字我们将其称为 未连接套接字 ,相比未连接套接字,连接套接字有以下几点不同:

  • 调用 sendto 时无需指定地址。但是可以使用 write 或者 send

  • 只有 connect 的目的地址和端口才能给自己发消息

  • 可以收到 ICMP 目的不可达的消息

  • 外出接口是固定的

实际上,连接套接字在调用 sendto 时可以指定地址,但是结果有两种:要么 sendto 中的地址覆盖了前面指定的地址,要么 sendto 返回 -1,并置 errno == EISCONN

实际上,只有连接套接字才能发送消息,一个未连接的套接字发送数据的流程为:

Diagram

每次连接都需要往内核拷贝一次套接字地址结构体。显然,如果多次调用 sendto 发送的地址和端口相同,使用连接套接字效率更好。

当目的服务器无法连接时,ICMP 目的不可达信息被映射为 ECONNREFUSED 错误,错误消息为:Connection refused

地址重用

地址重用使得多个进程可以监听同一个端口号,Linux 内核会将数据包随机发送给监听了此端口号的进程。

int val = 1;
ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &val, sizeof(val));
assert(ret != -1);

HINT: 一般而言,地址重用用作负载均衡。

故障处理

如果计算机中发送 UDP 包提示网络不可达,监听 UDP 套接字提示地址已被占用,那么有以下几种情况:

  • 没有连接网线或者无线网络

  • 网卡不支持组播功能

  • 机器没有可用的组播功能

  • 没有为主机添加路由,有些系统默认不添加 :

    sudo route add -net 224.0.0.0 netmask 224.0.0.0 dev enp0s20f0u1

此外,如果 setsockopt 报错:No such device,除了上述原因,也可以通过为端口使用确定的接口 IP 解决

在多端口情况下,UDP 的源 IP 可能会发生变化,这时需要特殊手段来固定源 IP,否则对方机器可能将其丢弃。参见 docker 容器网络下 UDP 协议的一个问题

另一个问题是 UDP 包的 TTL 问题,部分机器设置的 TTL 不足,导致 UDP 包无法发出机器,或者是发出机器后立即被抛弃。

组播

组播使用 UDP 发送数据,要求交换机介入。组播的流程为:

Diagram

加入组播组有两个方式:

  • 使用 IP_ADD_MEMBERSHIP:

    ip_mreq mreq;
    bzero(&mreq, sizeof(mreq));
    mreq.imr_interface.s_addr = htonl(INADDR_ANY);
    mreq.imr_multiaddr.s_addr = inet_addr("224.3.6.9");
    
    ret = setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
    assert(ret != -1);
  • 使用 MCAST_JOIN_GROUP:

    group_req mreq;
    bzero(&mreq, sizeof(mreq));
    
    char addr[] = "224.3.6.9";
    memcpy(&mreq.gr_group, addr, sizeof(addr));
    ret = setsockopt(sockfd, IPPROTO_IP, MCAST_JOIN_GROUP, &mreq, sizeof(mreq));
    assert(ret != -1);

两种加组效果相同。

TTL

多播套接字的默认 TTL 为 1,要更改 TTL,需要使用 IP_MULTICAST_TTL。

int val = 128;
ret     = setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &val, sizeof(val));
assert(ret != -1);

IP_MULTICAST_TTL 和 IP_TTL 更改的都是 IP 报文首部的 TTL 字段。但是操作系统有意将更改此字段的接口分成了两份。因为同一个 UDP 套接字既可以单播,也可以多播,而单播和多播的报文转发策略通常不同

bind 和 connect

组播既可以调用 bind 也可以调用 connect

bind 的时候绑定的地址为外出接口的地址,绑定的端口为地址结构体中的端口

bind 、connect 和加组动作三者的顺序不做要求

广播

广播使用 UDP 发送数据,广播和组播的不同点在于不需要交换机的参与。当使用受限广播地址时,即使局域网内没有交换机也可以。广播的流程为:

Diagram

代码示范如下:

int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
assert(sockfd != -1);

sockaddr_in serveraddr;
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(9321);
int ret             = inet_pton(AF_INET, "192.168.1.255", &serveraddr.sin_addr);
assert(ret != -1);

int val = 1;
ret     = setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
assert(ret != -1);

int val2 = 128;
ret      = setsockopt(sockfd, IPPROTO_IP, IP_TTL, &val2, sizeof(val2));
assert(ret != -1);

while(true) {
    sendto(sockfd, "hahaha", 7, 0, (sockaddr *)&serveraddr, sizeof(serveraddr));
    usleep(1000 * 500);
    printf("发送广播成功\n");
}

广播的 TTL 也是通过 IP_TTL 选项设置的。

广播的 bind 和 connect

广播也可以调用 bind 和 connect 函数

bind 的时候绑定的地址为外出接口的地址,绑定的端口为地址结构体中的端口

connect 函数的调用必须在获得广播权力后才能执行,否则提示 Permission denied

bind 和启用广播能力两者顺序不做要求,connect 需要在启用广播能力之后。这也侧面证明了 UDP 发送报文前需要变成连接套接字。

KCP

KCP 是应用层通信协议,底层协议一般使用 TCP。KCP 专注于减少延迟而不是减少带宽。

pgm

可靠多播 (pgm) 相比普通多播而言添加了可靠性

DPDK

DPDK 是用户层网络 IO 中间层

非阻塞文件描述符

一个非阻塞套接字在以下三个步骤是非阻塞的:

  • connect 时非阻塞

  • read 时非阻塞

  • write 时非阻塞

设置一个套接字为非阻塞的方式为:

static void SetSocketNonBlocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);

    // activate TCP_NODELAY
    int tcpNoDelay = 1;
    setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (char*)&tcpNoDelay, sizeof(int));
}

非阻塞套接字时三种函数具有不同的行为:

  • connect 返回三种返回值:成功返回零,正在连接返回 -1

  • read 成功时返回读取的字节数,失败时返回 -1

  • write 成功时返回写入的字节数,失败时返回 -1

另外,如果 connect 连接过程中被信号中断,返回值为 EINTR,这种情况下继续连接即可

一般而言,在调用 connect 后需要调用 select 来判断套接字是否可写,可写则表明成功

读取一个非阻塞套接字的方式为:

std::string Read() {
   if(fd_ < 0) return {};

   int         ret = 0;
   char        buf[1024];
   std::string result;

   do {
       ret = recvfrom(fd_, buf, sizeof(buf), 0, nullptr, nullptr);
       result.append(buf);
   } while((ret == -1) || (ret == 0));

   return result;
}

IO 多路复用

Select

select 是一种 IO 复用手段:

while(true) {
   sockaddr_in client;
   socklen_t   clientLen = sizeof(client);
   int         connfd    = accept(fd, (sockaddr*)&client, &clientLen);
   if(connfd == -1) {
      spdlog::error("连接客户端套接字失败");
      continue;
   }
   char buffer[1024];
   // 进入 select 阶段
   fd_set readFds;
   fd_set exceptionFds;
   FD_ZERO(&readFds);
   FD_ZERO(&exceptionFds);
   while(true) {
      memset(buffer, '\0', sizeof(buffer));
      FD_SET(connfd, &readFds);
      FD_SET(connfd, &exceptionFds);
      if(select(connfd + 1, &readFds, nullptr, &exceptionFds, nullptr) < 0) {
            spdlog::error("Select 失败");
            break;
      }
      // 解析可读事件
      if(FD_ISSET(connfd, &readFds)) {
            if(recv(connfd, buffer, sizeof(buffer) - 1, 0) <= 0) break;
            std::cout << "得到数据:" << buffer << std::endl;
      }
      if(FD_ISSET(connfd, &exceptionFds)) {
            if(recv(connfd, buffer, sizeof(buffer) - 1, MSG_OOB) <= 0) break;
            std::cout << "得到 OOB 数据:" << buffer << std::endl;
      }
   }
   close(connfd);
}

select 唯一能够处理的异常情况就是带外数据

epoll

epoll 可以支持的最大文件描述符数量为 /proc/sys/fs/epoll/max_user_watches

epoll 使用红黑树监听并维护所有文件描述符,但是还额外维护一个活跃链表。当某个监听的套接字活跃时,操作系统通过回调函数将活跃的套接字插入到此 list 中,因此 epoll_wait 只需要从内核态拷贝少量句柄到用户态即可

epoll相比于select并不是在所有情况下都要高效,例如在如果有少于1024个文件描述符监听,且大多数socket都是出于活跃繁忙的状态,这种情况下,select要比epoll更为高效,因为epoll会有更多次的系统调用,用户态和内核态会有更加频繁的切换。[1]

epoll高效的本质在于:

  • 减少了用户态和内核态的文件句柄拷贝

  • 减少了对可读可写文件句柄的遍历

  • mmap 加速了内核与用户空间的信息传递,epoll是通过内核与用户mmap同一块内存,避免了无谓的内存拷贝

  • IO性能不会随着监听的文件描述的数量增长而下降

  • 使用红黑树存储fd,以及对应的回调函数,其插入,查找,删除的性能不错,相比于hash,不必预先分配很多的空间

epoll 使用流程可以简述为:

Diagram

epoll 具有 lt 和 et 两种模式:

#include <spdlog/spdlog.h>

#include <iostream>
#include <string>
#include <thread>

extern "C" {
#include <arpa/inet.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/un.h>
#include <unistd.h>
}
#define BUFFER_SIZE      10
#define MAX_EVENT_NUMBER 1024
// 将文件描述符设置为非阻塞方式
int setNonBlocking(int fd) {
   int oldOption = fcntl(fd, F_GETFL);
   int newOption = oldOption | O_NONBLOCK;
   fcntl(fd, newOption);
   return oldOption;
}
// 将 fd 上的 EPOLLIN 注册到 efd 指向的 epoll 内核事件表中
void addFd(int epfd, int fd, bool enableEt) {
   epoll_event event;
   event.data.fd = fd;
   event.events  = EPOLLIN;
   if(enableEt) event.events |= EPOLLET;
   epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
   setNonBlocking(fd);
}

// LT 模式的工作流程
void ltMode(epoll_event* events, int number, int epfd, int listenfd) {
   char buf[BUFFER_SIZE];
   for(int i = 0; i < number; ++i) {
      int sockFd = events[i].data.fd;
      if(sockFd == listenfd) {
            sockaddr_in clientAddress;
            socklen_t   clientAddrLen = sizeof(clientAddress);
            int         connfd        = accept(listenfd, (sockaddr*)&clientAddress, &clientAddrLen);
            addFd(epfd, connfd, false);
      } else if(events[i].events & EPOLLIN) {
            // 读取数据
            std::cout << "触发事件" << std::endl;
            memset(buf, '\0', BUFFER_SIZE);
            if(recv(sockFd, buf, BUFFER_SIZE - 1, 0) <= 0) continue;
            std::cout << "得到数据:" << buf << std::endl;
      } else {
            std::cout << "没有事件发生" << std::endl;
      }
   }
}

void etMode(epoll_event* events, int number, int epfd, int listenfd) {
   char buf[BUFFER_SIZE];
   for(int i = 0; i < number; ++i) {
      int sockfd = events[i].data.fd;
      if(sockfd == listenfd) {
            sockaddr_in clientAddress;
            socklen_t   clientAddrLen = sizeof(clientAddress);
            int         connfd        = accept(listenfd, (sockaddr*)&clientAddress, &clientAddrLen);
            addFd(epfd, connfd, true);
      } else if(events[i].events & EPOLLIN) {
            // 这段代码不会被重复出发,因此一次就要独读出所有数据
            std::cout << "触发事件\n";
            while(true) {
               memset(buf, '\0', BUFFER_SIZE);
               int ret = recv(sockfd, buf, BUFFER_SIZE - 1, 0);
               if(ret < 0) {
                  if((errno == EAGAIN) || (errno == EWOULDBLOCK)) { std::cout << "数据读取完毕\n"; }
                  close(sockfd);
                  break;
               } else if(ret == 0)
                  close(sockfd);
               else
                  std::cout << "得到数据:" << buf << std::endl;
            }
      } else {
            std::cout << "没有触发事件" << std::endl;
      }
   }
}

int main() {
   sockaddr_in address;
   bzero(&address, sizeof(address));
   address.sin_family = AF_INET;
   inet_pton(AF_INET, "127.0.0.1", &address.sin_addr);
   address.sin_port = htons(9020);

   int fd = socket(PF_INET, SOCK_STREAM, 0);
   if(fd == -1) {
      spdlog::critical("套接字创建失败");
      exit(-1);
   }
   if(bind(fd, (sockaddr*)&address, sizeof(address)) == -1) {
      spdlog::critical("绑定端口失败");
      exit(-1);
   }
   if(listen(fd, 5) == -1) {
      spdlog::critical("监听失败");
      exit(-1);
   }

   epoll_event events[MAX_EVENT_NUMBER];
   int         epfd = epoll_create(5);
   if(epfd == -1) {
      spdlog::critical("创建 epoll 失败");
      exit(-1);
   }
   addFd(epfd, fd, true);
   while(true) {
      int ret = epoll_wait(epfd, events, MAX_EVENT_NUMBER, -1);
      if(ret < 0) {
            std::cout << "epoll 失败\n";
            break;
      }
      // ltMode(events, ret, epfd, fd);
      etMode(events, ret, epfd, fd);

   }
   close(fd);
   return 0;
}

也就是说边缘触发只触发一次,水平触发会一直触发

  • ET 模式中的文件描述符应当是非阻塞的,否则读写操作会因为没有后续事件而一直处于阻塞状态

  • 边缘触发的情况下,需要一次性将文件描述符中的数据全部读出。水平触发的情况下可以一次只读取一定量数据

bond 网卡

将两个物理网卡 bonding 到一起,会形成一个 bond 网卡,此网卡的名字为 bondX。这时两个物理网卡都没有 ip 地址,只有 bond 网卡有地址。绑定的网卡在 ifconfig 的状态中由 SLAVE 字样。

使用 cat /proc/net/bonding/bondX 可以看到网络的 bonding 模式。

在 bond 主备模式的情况下,如果将两个网卡分别插到两个交换机,而其中一个交换机网络不通。就可能导致由于网线插错导致的问题。

  • bond 网卡不需要设置混杂模式

  • bond 网卡的具备自己的 MAC 地址。即使是发生了网卡切换,也不会导致 TCP 链接断开。

mtu

三层交换机两端 mtu 不同可能导致网络不通

Last moify: 2025-01-16 10:33:22
Build time:2025-07-18 09:41:42
Powered By asphinx