<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>绿色记忆 &#187; 网络编程</title>
	<atom:link href="https://blog.gmem.cc/tag/%e7%bd%91%e7%bb%9c%e7%bc%96%e7%a8%8b/feed" rel="self" type="application/rss+xml" />
	<link>https://blog.gmem.cc</link>
	<description></description>
	<lastBuildDate>Fri, 17 Apr 2026 09:20:32 +0000</lastBuildDate>
	<language>en-US</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.9.14</generator>
	<item>
		<title>libevent学习笔记</title>
		<link>https://blog.gmem.cc/libevent-study-note</link>
		<comments>https://blog.gmem.cc/libevent-study-note#comments</comments>
		<pubDate>Wed, 21 Feb 2018 03:49:55 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25325</guid>
		<description><![CDATA[<p>简介 libevent是一个事件驱动编程库，可以在文件描述符上发生特定事件、超时后，执行相应的回调函数。回调函数还可以由信号、定时器触发。 使用libevent后，你不需要为事件驱动的网络服务编写事件循环，而只需要调用[crayon-69e20248ba4ab382360388-i/]并动态注册/删除事件。 libevent目前支持/dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), epoll(4)等I/O多路复用机制，这些底层机制和libevent提供的API完全解耦，你不需要关心。libevent可以根据系统选择最适合的机制。 你可以在多线程环境下使用libevent，并选取以下编程风格中的一种： 每个线程独立使用一个[crayon-69e20248ba4b2121268735-i/] 使用共享的event_base并使用锁保护 除了基础的事件轮询之外，libevent还实现了精巧的网络IO框架，支持套接字、过滤器、限速、SSL、零拷贝文件传输、IOCP，libevent内置对DNS、HTTP的支持，还提供了一个小型的RPC框架。 安装 [crayon-69e20248ba4b5796857846/] epoll 本章以Linux下的epoll为例，了解I/O多路复用技术的原理。  支持I/O操作的内核对象，都属于流（Stream），包括文件、管道、套接字。操控流时需要使用其文件描述符。当流中没有数据时，读线程会阻塞；当流已经写满数据时，写线程会阻塞。 <a class="read-more" href="https://blog.gmem.cc/libevent-study-note">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/libevent-study-note">libevent学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">简介</span></div>
<p>libevent是一个事件驱动编程库，可以在文件描述符上发生<span style="background-color: #c0c0c0;">特定事件、超时</span>后，执行相应的回调函数。回调函数还可以由<span style="background-color: #c0c0c0;">信号、定时器触发</span>。</p>
<p>使用libevent后，你不需要为事件驱动的网络服务编写事件循环，而只需要调用<pre class="crayon-plain-tag">event_dispatch()</pre>并动态注册/删除事件。</p>
<p>libevent目前支持/dev/poll, kqueue(2), event ports, POSIX select(2), Windows select(), poll(2), epoll(4)等I/O多路复用机制，这些底层机制和libevent提供的API完全解耦，你不需要关心。libevent可以根据系统选择最适合的机制。</p>
<p>你可以在多线程环境下使用libevent，并选取以下编程风格中的一种：</p>
<ol>
<li>每个线程独立使用一个<pre class="crayon-plain-tag">event_base</pre></li>
<li>使用共享的event_base并使用锁保护</li>
</ol>
<p>除了基础的事件轮询之外，libevent还实现了精巧的<span style="background-color: #c0c0c0;">网络IO框架</span>，支持套接字、过滤器、限速、SSL、零拷贝文件传输、IOCP，libevent内置对<span style="background-color: #c0c0c0;">DNS、HTTP的支持</span>，还提供了一个小型的<span style="background-color: #c0c0c0;">RPC框架</span>。</p>
<div class="blog_h1"><span class="graybg">安装</span></div>
<pre class="crayon-plain-tag">wget https://github.com/libevent/libevent/releases/download/release-2.1.8-stable/libevent-2.1.8-stable.tar.gz
tar -zxvf libevent-2.1.8-stable.tar.gz
pushd libevent-2.1.8-stable
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make
sudo make install</pre>
<div class="blog_h1"><span class="graybg">epoll</span></div>
<p>本章以Linux下的epoll为例，了解I/O多路复用技术的原理。 </p>
<p>支持I/O操作的内核对象，都属于流（Stream），包括<span style="background-color: #c0c0c0;">文件、管道、套接字</span>。操控流时需要使用其<span style="background-color: #c0c0c0;">文件描述符</span>。当流中没有数据时，读线程会阻塞；当流已经写满数据时，写线程会阻塞。</p>
<p>为了解决阻塞的问题，Linux提供了多种系统调用，包括select、epoll。后者目前被广泛使用，它的优势是：</p>
<ol>
<li>无需遍历被监控文件描述符的完整集合，只需要关注活动的连接</li>
<li>支持处理大量连接请求而不出现性能下降</li>
</ol>
<div class="blog_h2"><span class="graybg">触发模式</span></div>
<p>epoll支持两种事件触发模式：</p>
<ol>
<li>水平触发：内核事件集会拷贝给用户态事件，假设用户处理了一部分，那些<span style="background-color: #c0c0c0;">没有处理的会在下次epoll_wait再次返回</span></li>
<li>边缘触发：任何内核事件仅仅会被通知一次。拷贝减少性能增加，但是用户代码如果没有处理事件，会导致事件丢失</li>
</ol>
<div class="blog_h2"><span class="graybg">主要接口</span></div>
<div class="blog_h3"><span class="graybg">创建epoll实例</span></div>
<pre class="crayon-plain-tag">// size 提示内核此epoll实例需要监控的fd的数量
// 返回epoll实例的文件描述符
int epoll_create(int size);

// 示例：
int epfd = epoll_create(1024);</pre>
<div class="blog_h3"><span class="graybg">控制epoll实例</span></div>
<pre class="crayon-plain-tag">// epfd 控制的epoll实例
// op 执行的控制行为：
//   EPOLL_CTL_ADD，注册新的fd到epfd
//   EPOLL_CTL_MOD，修改已经注册的fd的监听事件
//   EPOLL_CTL_DEL，删除一个fd
// 
// fd 目标文件描述符
// event 告诉内核需要监听的事件
// 
// 成功返回0，失败返回-1, errno查看错误信息
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

// 事件的结构如下：
struct epoll_event
{
  // EPOLLIN, EPOLLOUT, EPOLLPRI, EPOLLHUP, EPOLLET, EPOLLONESHOT等类型
  uint32_t events;    // 事件类型
  epoll_data_t data;  // 用户传递的数据
}
// 用户数据联合体
typedef union epoll_data
{
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;</pre>
<div class="blog_h3"><span class="graybg">等待epoll事件</span></div>
<pre class="crayon-plain-tag">// epfd 等待的epoll实例
// event 事件缓冲，内核将等到的事件存放到此数组中
// maxevents 最多获取多少个事件，不得大于epoll_create()调用时指定的size
// timeout 超时，超过指定的时间从等待返回：
//         -1: 永久阻塞   0: 立即返回，非阻塞     &gt;0: 指定微秒数
// 返回 就绪的文件描述符数量
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int timeout);

// 示例：
struct epoll_event event_buffer[512];
int event_count = epoll_wait(epfd, epoll_event, 512, -1);</pre>
<div class="blog_h2"><span class="graybg">编程模板 </span></div>
<pre class="crayon-plain-tag">// 创建实例
int epfd = epoll_crete(1024);

// 将监听套接字的文件描述符加入监控
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &amp;listen_event);

while (1) {
    // 等待就绪
    int event_count = epoll_wait(epfd, events, 512, -1);
    // 遍历就绪事件列表
    for (i = 0 ; i &lt; event_count; i++) {
        if (evnets[i].data.fd == listen_fd) {
            // 这是监听套接字上的事件，accept连接，并将连接套接字的fd加入到epoll
        }
        else if (events[i].events &amp; EPOLLIN) {
            // 可读
        }
        else if (events[i].events &amp; EPOLLOUT) {
            // 可写
        }
    }
}</pre>
<div class="blog_h2"><span class="graybg">示例代码</span></div>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;ctype.h&gt;
#include &lt;string.h&gt;

#include &lt;unistd.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;

#include &lt;sys/epoll.h&gt;

#define SERVER_PORT         (8080)
#define SERVER_IP           ("127.0.0.1")
#define EPOLL_MAX_NUM       (1024)
#define BUFFER_MAX_LEN      (4096)

char buffer[BUFFER_MAX_LEN];

int main(int argc, char **argv) {
  int listen_fd = 0;
  int client_fd = 0;
  struct sockaddr_in server_addr;
  struct sockaddr_in client_addr;
  socklen_t client_len;

  int epfd = 0;
  struct epoll_event event, *events_buffer;

  // 创建套接字
  listen_fd = socket(AF_INET, SOCK_STREAM, 0);

  // 绑定到指定地址和端口
  server_addr.sin_family = AF_INET;
  // 展现形式（p，字符串）转换为数字形式（n）
  inet_pton(AF_INET, SERVER_IP, &amp;server_addr.sin_addr);
  server_addr.sin_port = htons(SERVER_PORT);
  bind(listen_fd, (struct sockaddr *) &amp;server_addr, sizeof(server_addr));

  // 开始监听连接
  listen(listen_fd, 10);

  // 创建epoll实例
  epfd = epoll_create(EPOLL_MAX_NUM);
  if (epfd &lt; 0)
    goto END;

  // 将监听套接字的可读事件加入到epoll
  event.events = EPOLLIN;
  event.data.fd = listen_fd;
  if (epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &amp;event) &lt; 0)
    goto END;

  // 分配事件缓冲
  events_buffer = malloc(sizeof(struct epoll_event)*EPOLL_MAX_NUM);

  // 事件循环
  while (1) {
    // 等待一批事件，有事件就可能返回，不一定要等待到EPOLL_MAX_NUM
    int active_fds_cnt = epoll_wait(epfd, events_buffer, EPOLL_MAX_NUM, 1000);
    for (int i = 0; i &lt; active_fds_cnt; i++) {
      // 监听套接字就绪
      if (events_buffer[i].data.fd==listen_fd) {
        // 接受新连接
        client_fd = accept(listen_fd, (struct sockaddr *) &amp;client_addr, &amp;client_len);
        if (client_fd &lt; 0)
          continue;

        char ip[20];
        // 数字转换为展现形式
        const char *client_ip = inet_ntop(AF_INET, &amp;client_addr.sin_addr, ip, sizeof(ip));
        // 网络序转换为主机序
        uint16_t client_port = ntohs(client_addr.sin_port);
        printf("new connection %s:%d accepted\n", client_ip, client_port);

        // 将连接套接字加入epoll，关注其可读可写事件
        event.events = EPOLLIN | EPOLLET;
        event.data.fd = client_fd;
        epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &amp;event);
      } else if (events_buffer[i].events &amp; EPOLLIN) {
        // 连接套接字就绪，可读
        client_fd = events_buffer[i].data.fd;

        buffer[0] = '\0';
        // 读入最多5字节
        int n = read(client_fd, buffer, 5);
        if (n &lt; 0) {
          // 读取失败
          continue;
        } else if (n==0) {
          // 读取到0字节，说明EOF，关闭连接并注册epoll监听
          epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &amp;event);
          close(client_fd);
        } else {
          // 读取到数据
          printf("data from client: %s\n", buffer);
          // 原样写回去
          buffer[n] = '\0';
          // 这是阻塞写
          write(client_fd, buffer, strlen(buffer));
          // 重置缓冲区
          memset(buffer, 0, BUFFER_MAX_LEN);
        }
      } else if (events_buffer[i].events &amp; EPOLLOUT) {
        // 在这里进行非阻塞写
      }
    }
  }

  END:
  close(epfd);
  close(listen_fd);
  return 0;
} </pre>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
#include &lt;strings.h&gt;

#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;unistd.h&gt;
#include &lt;fcntl.h&gt;

#define BUFFER_MAX_LEN      (1024)
#define SERVER_PORT         (8080)
#define SERVER_IP           ("127.0.0.1")

int main(int argc, char **argv) {
  int sockfd;
  char recvbuf[BUFFER_MAX_LEN + 1] = {0};

  struct sockaddr_in server_addr;

  // 创建套接字
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) &lt; 0)
    exit(0);

  // 服务器地址
  bzero(&amp;server_addr, sizeof(server_addr));
  server_addr.sin_family = AF_INET;
  server_addr.sin_port = htons(SERVER_PORT);
  inet_pton(AF_INET, SERVER_IP, &amp;server_addr.sin_addr);

  // 连接到服务器
  if (connect(sockfd, (struct sockaddr *) &amp;server_addr, sizeof(server_addr)) &lt; 0) {
    fprintf(stderr, "failed to connect to server \n");
    exit(0);
  }

  char input[100];

  // 从标准输入读取输入
  while (fgets(input, 100, stdin)!=NULL) {
    send(sockfd, input, strlen(input), 0);
    // 读取服务器输出
    read(sockfd, recvbuf, BUFFER_MAX_LEN);
    printf("echo %s\n", recvbuf);
  }

}</pre>
<div class="blog_h1"><span class="graybg">libevent基础</span></div>
<div class="blog_h2"><span class="graybg">event_base</span></div>
<p>使用 libevent 函数之前需要分配一个或者多个event_base结构。此结构持有libevent分发循环（dispatch loop）的信息和状态，它是libevent的中心，跟踪所有未决、活动事件并将活动事件通知给应用程序。</p>
<p>event_base是一个不透明结构（其字段没有定义在头文件中）。调用<pre class="crayon-plain-tag">event_base_new()</pre>或<pre class="crayon-plain-tag">event_base_new_with_config()</pre>可以实例化event_base。</p>
<p>配合锁，你可以在多个线程中访问event_base，但是，其<span style="background-color: #c0c0c0;">事件循环只能在单个线程中执行</span>。</p>
<p>event_base可以选择一种IO多路复用技术，作为检测事件是否就绪的手段。这些IO多路复用技术包括： select, poll, epoll, kqueue, devpoll, evport, win32</p>
<div class="blog_h3"><span class="graybg">实例化</span></div>
<p>大部分情况下，调用下面的函数即可：</p>
<pre class="crayon-plain-tag">// 创建并返回一个具有默认设置的event_base
// 如果出错返回NULL
struct event_base *event_base_new(void);</pre>
<p>如果要定制even_base的特性，可以调用：</p>
<pre class="crayon-plain-tag">// 创建一个配置
struct event_config *event_config_new(void);

// 定制化
// 禁止使用某种IO多路复用机制
int event_config_avoid_method(struct event_config *cfg, const char *method);
// 要求IO多路复用机制支持某些特性，不支持特性的后端不被使用
enum event_method_feature {
    EV_FEATURE_ET = 0x01,    // 支持边缘触发
    EV_FEATURE_O1 = 0x02,    // 添加、删除单个事件，以及确定哪个事件是激活的 —— 这些操作必须是O(1)复杂度
    EV_FEATURE_FDS = 0x04,   // 支持任何文件描述符，而不仅仅是套接字
};
int event_config_require_features(struct event_config *cfg, enum event_method_feature feature);
// 设置标记
enum event_base_config_flag {
    EVENT_BASE_FLAG_NOLOCK = 0x01, // 不要为 event_base 分配锁，节省加解锁开销，但是多线程访问不安全
    EVENT_BASE_FLAG_IGNORE_ENV = 0x02, // 选择使用的后端时,不要检测 EVENT* 环境变量
    EVENT_BASE_FLAG_NO_CACHE_TIME = 0x08,
    EVENT_BASE_FLAG_EPOLL_USE_CHANGELIST = 0x10, // 如果选择了epoll后端，则使用更快的epoll-changelist。不得传递基于dup()或其变体克隆的文件描述符给lbevent
    EVENT_BASE_FLAG_PRECISE_TIMER = 0x20
};
int event_config_set_flag(struct event_config *cfg, enum event_base_config_flag flag);


// 使用配置实例化
struct event_base * event_base_new_with_config(const struct event_config *cfg);</pre>
<div class="blog_h3"><span class="graybg">销毁</span></div>
<pre class="crayon-plain-tag">// 该函数不会释放与 event_base 关联的任何事件，或者关闭他们的套接字，或者释放任何指针
void event_base_free(struct event_base *base);</pre>
<div class="blog_h3"><span class="graybg">优先级 </span></div>
<p>libevent支持为事件设置多个优先级。默认情况下event_base仅仅支持单个优先级，调用下面的方法设置优先级数量：</p>
<pre class="crayon-plain-tag">int event_base_priority_init(struct event_base *base, int n_priorities);</pre>
<p>新的事件可以使用[0,n_priorities-1]这个范围的优先级，值越低优先级越高。</p>
<div class="blog_h3"><span class="graybg">子进程</span></div>
<p>某些事件后端，在fork后不能正常工作，你可能需要重现初始化event_base：</p>
<pre class="crayon-plain-tag">struct event_base *base = event_base_new();

if (fork()) {
    continue_running_parent(base);
} else {
    // 子进程
    event_reinit(base);
    continue_running_child(base);
}</pre>
<div class="blog_h2"><span class="graybg">事件</span></div>
<p>事件是libevent操控的基本单元，事件代表一组条件的<span style="background-color: #c0c0c0;">集合</span>，<span style="background-color: #c0c0c0;">这些条件包括</span>：</p>
<ol>
<li>文件描述符已经就绪，可以读取或者写入</li>
<li>对于边缘触发：文件描述符变为就绪状态，可以读取或者写入</li>
<li>超时</li>
<li>信号</li>
<li>用户触发事件</li>
</ol>
<p>所有事件具有相似的生命周期：</p>
<ol>
<li>创建事件（并关联到event_base）后，变为initialized状态</li>
<li>添加到event_base中后，变为pending状态</li>
<li>触发事件的条件发生后，变为active状态。事件的回调会执行</li>
<li>如果将事件配置为<span style="background-color: #c0c0c0;">persistent</span>，则它总是保持在pending状态。<span style="background-color: #c0c0c0;">否则，执行回调后，事件不再是pending</span></li>
<li>从event_base删除事件后，它从pending变为initialized。再次添加则由变为pending</li>
</ol>
<div class="blog_h3"><span class="graybg">创建</span></div>
<p>调用event_new函数可以创建事件：</p>
<pre class="crayon-plain-tag">// 指示超时已经发生
#define EV_TIMEOUT      0x01
// 等待套接字或FD可读
#define EV_READ         0x02
// 等待套接字或FD可写
#define EV_WRITE        0x04
// 等待POSIX信号发生
#define EV_SIGNAL       0x08
// 持久化事件 —— 不会因为激活而被自动移除
#define EV_PERSIST      0x10
// 使用边缘触发 
#define EV_ET           0x20

// 事件回调函数指针的定义
typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

// 分配一个事件并关联到event_base
// base 事件关联到的event_base
// fd 需要监控的文件描述符或者信号
// what 希望监控的条件、事件的性质、触发方式 EV_READ, EV_WRITE, EV_SIGNAL, EV_PERSIST, EV_ET
// cb 事件发生后执行的回调函数
// arg 回调函数的参数
struct event *event_new(struct event_base *base, evutil_socket_t fd, short what, event_callback_fn cb, void *arg);

void event_free(struct event *event);</pre>
<p>event_assign也可以用来创建事件，和event_new不同的是，前者<span style="background-color: #c0c0c0;">不负责分配内存，你必须自己分配好event结构</span>：</p>
<pre class="crayon-plain-tag">int event_assign(struct event *, struct event_base *, evutil_socket_t, short, event_callback_fn, void *);</pre>
<p>事件创建后，你可以调用<pre class="crayon-plain-tag">event_add()</pre>或<pre class="crayon-plain-tag">event_del()</pre> 来添加（使之pending）、删除（使之非pending）事件。</p>
<div class="blog_h3"><span class="graybg">创建信号事件</span></div>
<p>可以使用下面的宏：</p>
<pre class="crayon-plain-tag">#define evsignal_new(base, signum, cb, arg) \
    event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)</pre>
<p>对于大部分的IO多路复用机制，每个进程任何时刻<span style="background-color: #c0c0c0;">只能有一个 event_base 可以监听信号</span>。</p>
<div class="blog_h3"><span class="graybg">添加/删除事件</span></div>
<p>构造事件之后，在将其添加到 event_base 之前不能对其做任何操作。你需要调用：</p>
<pre class="crayon-plain-tag">// 如果tv==NULL则添加的事件不会超时
// 如果对已决事件调用该函数，则在tv后事件重新调度，此前保持未决状态
int event_add(struct event *ev, const struct timeval *tv);</pre>
<p>将事件加入event_base并使其进入未决（pending）状态。 </p>
<p>调用下面的函数，可以使事件变为非未决也非激活的：</p>
<pre class="crayon-plain-tag">int event_del(struct event *ev);</pre>
<div class="blog_h3"><span class="graybg">优先级</span></div>
<p>默认情况下，多个事件激活，其回调执行顺序是未知的。使用优先级，可以让某些事件优先执行：</p>
<pre class="crayon-plain-tag">int event_priority_set(struct event *event, int priority);</pre>
<p>注意：<span style="background-color: #c0c0c0;">多个不同优先级的事件同时激活时 ，低优先级的事件不会运行</span> 。libevent会执行高优先级的事件，然后<span style="background-color: #c0c0c0;">重新检查各个事件</span>。只有在没有高优先级的事件是激活的时候，低优先级的事件才有机会执行。</p>
<div class="blog_h3"><span class="graybg">检查事件状态</span></div>
<p>调用下面的函数，可以知晓目标事件是未决还是激活的：</p>
<pre class="crayon-plain-tag">int event_pending(const struct event *ev, short what, struct timeval *tv_out);</pre>
<div class="blog_h3"><span class="graybg">一次触发事件</span></div>
<p>执行下面的方法，可以调度一个”一次性“事件。</p>
<pre class="crayon-plain-tag">int event_base_once(struct event_base *, evutil_socket_t, short, event_callback_fn, void *, const struct timeval *);</pre>
<p>回调仅仅会执行一次。</p>
<div class="blog_h3"><span class="graybg">手工激活事件</span></div>
<p>即少数情况下，需要在条件不满足的情况下，强行触发一个事件</p>
<pre class="crayon-plain-tag">void event_active(struct event *ev, int what, short ncalls); </pre>
<div class="blog_h2"><span class="graybg">事件循环 </span></div>
<p>你一旦为<span style="background-color: #c0c0c0;">event_base注册了某些事件</span>， 你需要让libevent等待事件的发生，并在事件发生后进行通知。</p>
<div class="blog_h3"><span class="graybg">启动循环</span></div>
<p>调用下面的函数启动事件循环：</p>
<pre class="crayon-plain-tag">// 这些宏用作flags参数
// 阻塞，直到有事件激活，当所有事件的回调被执行后，退出事件循环
#define EVLOOP_ONCE             0x01
// 不阻塞，检查那些事件已经就绪，然后执行最高优先级的那些事件的回调，然后退出
#define EVLOOP_NONBLOCK         0x02
// 即使没有pending事件也不退出，必须调用event_base_loopexit()或event_base_loopbreak()来退出
#define EVLOOP_NO_EXIT_ON_EMPTY 0x04

// 等待活动（Active，激活）事件，并且运行它们的回调，此函数是event_base_dispatch()更灵活的版本
// 默认情况下，此事件循环会运行event base直到：
// 1、没有任何pending或active事件
// 2、或者，event_base_loopbreak()或event_base_loopexit()被调用
// 返回值：如果正常退出返回0，否则返回-1
int event_base_loop(struct event_base *base, int flags);</pre>
<p>你也可以调用简化版本：</p>
<pre class="crayon-plain-tag">int event_base_dispatch(struct event_base *base);</pre>
<div class="blog_h3"><span class="graybg">停止循环 </span></div>
<p>根据启动循环时指定的flags的不同，循环可能在<span style="background-color: #c0c0c0;">没有任何pending事件（也就是已经移除所有已注册事件）</span>后自动结束。或者，你可以调用下面的函数提前结束： </p>
<pre class="crayon-plain-tag">// 在给定的时间tv后停止循环，如果tv为NULL立即停止
// 注意：如果 event_base 当前正在执行任何激活事件的回调，则回调会继续运行，直到运行完所有激活事件的回调之才停止
int event_base_loopexit(struct event_base *base,  struct timeval *tv);
// 立即退出循环，如果正在执行回调，当前回调完成后立即停止，不考虑尚未处理的事件
int event_base_loopbreak(struct event_base *base);</pre>
<div class="blog_h1"><span class="graybg">bufferevent</span></div>
<p>libevent提供了一种通用的缓冲机制 —— bufferevent。前面章节介绍的事件，在底层传输接口就绪后立即执行回调，而<span style="background-color: #c0c0c0;">bufferevent在读写足够多的数据之后才执行回调</span>。</p>
<p>bufferevent由<span style="background-color: #c0c0c0;">一个底层传输接口（例如套接字）、一个读缓冲区、一个写缓冲区</span>组成。缓冲区的类型是<pre class="crayon-plain-tag">struct evbuffer</pre>。</p>
<div class="blog_h2"><span class="graybg">回调和水位</span></div>
<p>默认情况下：</p>
<ol>
<li>从底层传输端口<span style="background-color: #c0c0c0;">读取了任意量的数据之后会调用读取回调</span></li>
<li> 输出缓冲区中<span style="background-color: #c0c0c0;">足够量的数据被刷到底层传输端口后写入回调会被调用</span></li>
</ol>
<p>通过调整“水位”，可以覆盖上述默认行为。每个bufferevent具有4个水位：</p>
<ol>
<li>读取低水位：读缓冲区数据量高于此水位后，读取回调被调用。默认值0，导致一旦缓冲区有数据，立即调用回调</li>
<li>读取高水位：读缓冲区数据量高于此水位后，bufferevent不再将数据读取到缓冲区。默认值无限</li>
<li>写入低水位：写缓冲区数据量低于此水位后，写入回调被调用。默认值0，导致仅当缓冲区全部写入到底层传输接口后，才调用写入回调继续写入</li>
<li>写入高水位：没有直接使用。当一个bufferevent用作另外一个bufferevent的底层传输接口时有用</li>
</ol>
<p>下面的函数可以调整bufferevent的读或/和写水位：</p>
<pre class="crayon-plain-tag">// 对于高水位，0表示无限
void bufferevent_setwatermark(struct bufferevent *bufev, short events, size_t lowmark, size_t highmark);</pre>
<div class="blog_h2"><span class="graybg">基于套接字创建</span></div>
<p>调用下面的方式，创建基于套接字的bufferevent：</p>
<pre class="crayon-plain-tag">enum bufferevent_options {
	// 释放 bufferevent 时关闭底层传输端口，例如：关闭底层套接字、释放底层bufferevent
	BEV_OPT_CLOSE_ON_FREE = (1&lt;&lt;0),

	// 自动为 bufferevent 分配锁，使之可线程安全的访问
	BEV_OPT_THREADSAFE = (1&lt;&lt;1),

	// 所有回调在事件循环中延迟的调用
	BEV_OPT_DEFER_CALLBACKS = (1&lt;&lt;2),

	// 执行回调时不对bufferevent进行锁定
	BEV_OPT_UNLOCK_CALLBACKS = (1&lt;&lt;3)
};

struct bufferevent *bufferevent_socket_new(
    // 关联到的event_base
    struct event_base *base,
    // 套接字的描述符，如果希望以后设置此描述符，传入-1
    evutil_socket_t fd,
    enum bufferevent_options options);</pre>
<div class="blog_h2"><span class="graybg">设置回调</span></div>
<p>调用bufferevent_setcb可以修改bufferevent的一个或多个回调：</p>
<ol>
<li>readcb：缓冲区的数据可读（考虑水位）时调用</li>
<li>writecb：文件描述符可以写入时（考虑水位）时调用</li>
<li>eventcb：文件描述符上发生事件时调用</li>
</ol>
<p>参数cbarg为传递给所有回调的参数。注意此参数被上述三种回调共享，修改它会影响所有回调。</p>
<p>要禁用回调，传输NULL作为回调函数。</p>
<pre class="crayon-plain-tag">typedef void (*bufferevent_data_cb)(struct bufferevent *bev, void *ctx);
typedef void (*bufferevent_event_cb)(struct bufferevent *bev,
    short events, void *ctx);

void bufferevent_setcb(struct bufferevent *bufev,
    bufferevent_data_cb readcb, bufferevent_data_cb writecb,
    bufferevent_event_cb eventcb, void *cbarg);

void bufferevent_getcb(struct bufferevent *bufev,
    bufferevent_data_cb *readcb_ptr,
    bufferevent_data_cb *writecb_ptr,
    bufferevent_event_cb *eventcb_ptr,
    void **cbarg_ptr);</pre>
<div class="blog_h2"><span class="graybg">启禁事件</span></div>
<p>下面的函数用于启用、禁用某种事件：</p>
<pre class="crayon-plain-tag">void bufferevent_enable(struct bufferevent *bufev, short events);
void bufferevent_disable(struct bufferevent *bufev, short events);

short bufferevent_get_enabled(struct bufferevent *bufev);</pre>
<p>events可选EV_READ|EV_WRITE。如果没有启用读事件则bufferevent不会尝试读取数据到缓冲区，写事件类似。 </p>
<div class="blog_h2"><span class="graybg">启动连接</span></div>
<p>调用下面的方法可以向服务器发起一个连接：</p>
<pre class="crayon-plain-tag">int bufferevent_socket_connect(struct bufferevent *bev, struct sockaddr *address, int addrlen);</pre>
<p>如果执行此调用时：</p>
<ol>
<li>尚未为bufferevent设置套接字，则自动创建非阻塞套接字。</li>
<li>已经为bufferevent设置套接字，则提示套接字尚未连接，直到连接成功前不应对其进行读写操作</li>
</ol>
<p>示例代码：</p>
<pre class="crayon-plain-tag">#include &lt;event2/event.h&gt;
#include &lt;event2/bufferevent.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;string.h&gt;

// 事件回调
void eventcb(struct bufferevent *bev, short events, void *ptr)
{
    // 如果通过bufferevent_socket_connect()发起连接，将会收到BEV_EVENT_CONNECTED事件
    // 如果通过connect()连接，则报告为写入事件
    if (events &amp; BEV_EVENT_CONNECTED) {
         // 连接成功
    } else if (events &amp; BEV_EVENT_ERROR) {
         // 出现错误
    }
}

int main()
{
    struct event_base *base;
    struct bufferevent *bev;
    struct sockaddr_in sin;

    base = event_base_new();

    // 连接到服务器127.0.0.1:8080
    memset(&amp;sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = htonl(0x7f000001); 
    sin.sin_port = htons(8080);

    // 创建bufferevent
    bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
    // 设置回调
    bufferevent_setcb(bev, NULL, NULL, eventcb, NULL);
    // 发起连接
    if (bufferevent_socket_connect(bev, (struct sockaddr *)&amp;sin, sizeof(sin)) &lt; 0) {
        // 出错，释放bufferevent
        bufferevent_free(bev);
        return -1;
    }

    // 启动事件循环
    event_base_dispatch(base);
    return 0;
}</pre>
<div class="blog_h2"><span class="graybg">读写数据</span> </div>
<div class="blog_h3"><span class="graybg">得到缓冲</span></div>
<p>调用下面的函数，可以获得bufferevent的读写缓冲区：</p>
<pre class="crayon-plain-tag">// 读缓冲
struct evbuffer *bufferevent_get_input(struct bufferevent *bufev);
// 写缓冲
struct evbuffer *bufferevent_get_output(struct bufferevent *bufev);

// 注意，不得在这些缓冲区上设置回调</pre>
<div class="blog_h3"><span class="graybg">写入</span></div>
<pre class="crayon-plain-tag">// 将data的前size个字节写入输出缓冲区
int bufferevent_write(struct bufferevent *bufev, const void *data, size_t size);
// 移除buf中所有内容，将其放入输出缓冲区的尾部
int bufferevent_write_buffer(struct bufferevent *bufev, struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">读取 </span></div>
<pre class="crayon-plain-tag">// 读取并移除输入缓冲区的最多size字节，并转储到data中，返回实际读取（移除）的字节数
size_t bufferevent_read(struct bufferevent *bufev, void *data, size_t size);
// 抽取输入缓冲区中所有数据，存放到buf中
int bufferevent_read_buffer(struct bufferevent *bufev, struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">刷出</span></div>
<p>刷出（flush）读写缓冲，意味着需要尽可能的：</p>
<ol>
<li>从底层传输接口读取尽可能多的数据</li>
<li>将写缓冲区的数据尽可能写如底层传输接口 </li>
</ol>
<pre class="crayon-plain-tag">// iotype，可以是 EV_READ | EV_WRITE 表示需要读还是写
// state，BEV_FINISHED告知对端，没有更多数据需要发送了。BEV_NORMAL、BEV_FLUSH的含义依赖于bufferevent类型
int bufferevent_flush(struct bufferevent *bufev, short iotype, enum bufferevent_flush_mode state);</pre>
<div class="blog_h2"><span class="graybg">操控evbuffer</span></div>
<p>读写bufferevnet的数据，操作的主要类型是结构evbuffer。evbuffer实现了一种优化的字节队列，支持从尾部追加数据，或从头部移除数据。</p>
<p>evbuffer用于缓冲网络I/O的缓冲区的处理。IO调度、触发由bufferevent负责。</p>
<div class="blog_h3"><span class="graybg">创建缓冲</span></div>
<pre class="crayon-plain-tag">struct evbuffer *evbuffer_new(void);</pre>
<div class="blog_h3"><span class="graybg">删除缓冲</span></div>
<pre class="crayon-plain-tag">void evbuffer_free(struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">线程安全</span></div>
<p>默认情况下，在多线程中访问evbuffer是不安全的，除非调用下面的方法启用锁：</p>
<pre class="crayon-plain-tag">// 如果lock为NULL，则libevent利用evthread_set_lock_creation_callback创建锁
int evbuffer_enable_locking(struct evbuffer *buf, void *lock);</pre>
<p>要启用、禁用锁，调用下面的函数：</p>
<pre class="crayon-plain-tag">void evbuffer_lock(struct evbuffer *buf);
void evbuffer_unlock(struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">检查长度</span></div>
<p>下面的函数返回evbuffer存储的字节数：</p>
<pre class="crayon-plain-tag">size_t evbuffer_get_length(const struct evbuffer *buf);</pre>
<div class="blog_h3"><span class="graybg">添加数据</span></div>
<pre class="crayon-plain-tag">// 从data处开始，写入datlen字节数进去
int evbuffer_add(struct evbuffer *buf, const void *data, size_t datlen);

// 格式化数据并全部写入
int evbuffer_add_printf(struct evbuffer *buf, const char *fmt, ...)
int evbuffer_add_vprintf(struct evbuffer *buf, const char *fmt, va_list ap);</pre>
<div class="blog_h3"><span class="graybg">扩充缓冲</span></div>
<pre class="crayon-plain-tag">int evbuffer_expand(struct evbuffer *buf, size_t datlen);</pre>
<p>上述函数确保evbuffer能足以容纳datlen字节，而不需要更多的内存分配 </p>
<div class="blog_h3"><span class="graybg">在缓冲之间移动数据</span></div>
<p>libevent提供了将数据从一个缓冲移动到另外一个的快捷方式：</p>
<pre class="crayon-plain-tag">// 将src中的数据移动到dst的尾部，成功返回0
int evbuffer_add_buffer(struct evbuffer *dst, struct evbuffer *src);
// 将src的datlen字节移动到dst的尾部
int evbuffer_remove_buffer(struct evbuffer *src, struct evbuffer *dst, size_t datlen); </pre>
<div class="blog_h2"><span class="graybg">释放</span></div>
<pre class="crayon-plain-tag">void bufferevent_free(struct bufferevent *bev);</pre>
<p>上面的函数用于释放bufferevent，bufferevent的内部具有引用计数机制，当上述函数调用时，尚有未决的延迟回调，则回调执行完毕之前不会删除bufferevent。</p>
<p>如果指定了BEV_OPT_CLOSE_ON_FREE，并且bufferevent使用套接字、其它bufferevent作为其底层传输接口，则它们会被一并关闭。 </p>
<div class="blog_h1"><span class="graybg">evconnlistener</span></div>
<p>在event2/listener.h中定义的连接监听器 —— evconnlistener，为libevent添加了监听/接受TCP连接的方法。</p>
<div class="blog_h2"><span class="graybg">创建</span></div>
<p>要创建新的连接监听器，调用：</p>
<pre class="crayon-plain-tag">struct evconnlistener * evconnlistener_new(
    // 关联的event_base
    struct event_base *base,
    // 新连接的回调，如果NULL则连接监听器是禁用的
    evconnlistener_cb cb, 
    // 传递给回调的参数
    void *ptr, 
    // 用于控制回调函数的行为
    unsigned flags, 
    // 尚未被接受的、排队的连接最大数量。如果为负数，libevent自动选择适当的值；如果为0，则认为提供的套接字的listen已经调用过
    int backlog,  
    // 监听套接字的FD
    evutil_socket_t fd
);

struct evconnlistener * evconnlistener_new_bind(struct event_base *base,
    evconnlistener_cb cb, void *ptr, unsigned flags, int backlog, 
    // 由libevent负责绑定监听套接字
    const struct sockaddr *sa, int socklen);


// 回调函数：
typedef void (*evconnlistener_cb)(struct evconnlistener *listener,
    // 新接受的套接字
    evutil_socket_t sock, 
    // 客户端地址和地址长度
    struct sockaddr *addr, int len, void *ptr); // ptr是先前提供的指针

// flags支持：

// 默认情况下，监听器接收到新连接后自动将其设置为非阻塞的。下面的标记禁用此行为
#define LEV_OPT_LEAVE_SOCKETS_BLOCKING	(1u&lt;&lt;0)
// 释放此连接监听器时，自动关闭底层套接字
#define LEV_OPT_CLOSE_ON_FREE		(1u&lt;&lt;1)
// 为底层套接字设置 close-on-exec 标志
#define LEV_OPT_CLOSE_ON_EXEC		(1u&lt;&lt;2)
// 某些平台在默认情况下，关闭某监听套接字后，需要经过一个超时，相同端口才可再次绑定，此选项禁用此行为，可以立即再次绑定
#define LEV_OPT_REUSEABLE		(1u&lt;&lt;3)
// 为连接监听器加锁，以便线程安全的访问
#define LEV_OPT_THREADSAFE		(1u&lt;&lt;4)
// 以禁用状态创建连接监听器，后续必须通过evconnlistener_enable()启用
#define LEV_OPT_DISABLED		(1u&lt;&lt;5)
// 提示libevent，如果可能的化，监听器应该延迟accept()连接，直到数据可用
#define LEV_OPT_DEFERRED_ACCEPT		(1u&lt;&lt;6)
// 提示允许多个服务器（进程/线程——绑定到相同端口
#define LEV_OPT_REUSEABLE_PORT		(1u&lt;&lt;7)</pre>
<p>连接监听器依赖于event_base，当监听套接字上有新的TCP连接后，event_base会通知evconnlistener。</p>
<div class="blog_h2"><span class="graybg">销毁</span></div>
<pre class="crayon-plain-tag">void evconnlistener_free(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">启禁</span></div>
<p>下面的函数可以禁止、重新允许连接监听器监听新连接：</p>
<pre class="crayon-plain-tag">int evconnlistener_disable(struct evconnlistener *lev);
int evconnlistener_enable(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">设置回调</span></div>
<pre class="crayon-plain-tag">void evconnlistener_set_cb(struct evconnlistener *lev, evconnlistener_cb cb, void *arg);</pre>
<div class="blog_h2"><span class="graybg">获取信息 </span></div>
<pre class="crayon-plain-tag">// 获取监听套接字
evutil_socket_t evconnlistener_get_fd(struct evconnlistener *lev);
// 获取关联的event_base
struct event_base *evconnlistener_get_base(struct evconnlistener *lev);</pre>
<div class="blog_h2"><span class="graybg">错误检测</span></div>
<p>当监听器accept失败后，可以调用预先设置的错误回调：</p>
<pre class="crayon-plain-tag">typedef void (*evconnlistener_errorcb)(struct evconnlistener *lis, void *ptr);
void evconnlistener_set_error_cb(struct evconnlistener *lev, evconnlistener_errorcb errorcb);</pre>
<div class="blog_h1"><span class="graybg">示例</span></div>
<div class="blog_h2"><span class="graybg">Echo服务器</span></div>
<pre class="crayon-plain-tag">#include &lt;string.h&gt;
#include &lt;errno.h&gt;
#include &lt;stdio.h&gt;
#include &lt;signal.h&gt;
#ifndef _WIN32
#include &lt;netinet/in.h&gt;
# ifdef _XOPEN_SOURCE_EXTENDED
#  include &lt;arpa/inet.h&gt;
# endif
#include &lt;sys/socket.h&gt;
#endif

#include &lt;event2/bufferevent.h&gt;
#include &lt;event2/buffer.h&gt;
#include &lt;event2/listener.h&gt;
#include &lt;event2/util.h&gt;
#include &lt;event2/event.h&gt;

static const char MESSAGE[] = "Hello, World!\n";

static const int PORT = 8080;

static void listener_cb(struct evconnlistener *, evutil_socket_t, struct sockaddr *, int socklen, void *);
static void conn_writecb(struct bufferevent *, void *);
static void conn_eventcb(struct bufferevent *, short, void *);
static void signal_cb(evutil_socket_t, short, void *);

int main(int argc, char **argv) {
  struct event_base *base;
  struct evconnlistener *listener;
  struct event *signal_event;
  struct sockaddr_in sin;
  // 创建event_base
  base = event_base_new();
  if (!base) {
    // 初始化libevent失败
    return 1;
  }

  memset(&amp;sin, 0, sizeof(sin));
  sin.sin_family = AF_INET;
  sin.sin_port = htons(PORT);
  // 创建连接监听器
  listener = evconnlistener_new_bind(base, listener_cb, (void *) base,
                                     LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1,
                                     (struct sockaddr *) &amp;sin,
                                     sizeof(sin));

  if (!listener) {
    // 创建连接监听器失败
    return 1;
  }
  // 添加中断（Ctrl-C）信号事件
  signal_event = evsignal_new(base, SIGINT, signal_cb, (void *) base);

  if (!signal_event || event_add(signal_event, NULL) &lt; 0) {
    // 创建或添加信号事件失败
    return 1;
  }
  // 开始执行事件循环，直到Ctrl-C不会退出
  event_base_dispatch(base);

  evconnlistener_free(listener);  // 销毁监听器
  event_free(signal_event);       // 销毁事件
  event_base_free(base);          // 销毁event_base

  return 0;
}
// 接收到新连接请求
static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd,
                        struct sockaddr *sa, int socklen, void *user_data) {
  struct event_base *base = user_data;
  struct bufferevent *bev;
  // 为此连接套接字创建bufferevent
  bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE);
  if (!bev) {
    // 创建失败，退出事件循环
    event_base_loopbreak(base);
    return;
  }
  bufferevent_setcb(bev, NULL, conn_writecb, conn_eventcb, NULL); // 设置回调
  bufferevent_enable(bev, EV_WRITE); // 启用写事件，支持向套接字写数据
  bufferevent_disable(bev, EV_READ); // 禁用读事件
  // 将需要发送给客户端的数据填充到bufferevent的缓冲中
  // 当文件描述符（客户端连接）可写时自动刷出
  bufferevent_write(bev, MESSAGE, strlen(MESSAGE));
}
// 可以向底层连接写入数据了
static void conn_writecb(struct bufferevent *bev, void *user_data) {
  struct evbuffer *output = bufferevent_get_output(bev);
  if (evbuffer_get_length(output)==0) {
    // 输出缓冲已经自动刷空
    // 关闭bufferevent，这里会导致底层连接被关闭
    bufferevent_free(bev);
  }
}

static void conn_eventcb(struct bufferevent *bev, short events, void *user_data) {
  if (events &amp; BEV_EVENT_EOF) {
    // 连接被关闭
  } else if (events &amp; BEV_EVENT_ERROR) {
    // 连接上出现错误
  }
  bufferevent_free(bev);
}

static void signal_cb(evutil_socket_t sig, short events, void *user_data) {
  struct event_base *base = user_data;
  // 接收到信号，两秒后退出事件循环
  struct timeval delay = {2, 0};
  event_base_loopexit(base, &amp;delay);
}</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/libevent-study-note">libevent学习笔记</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/libevent-study-note/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Go语言网络编程</title>
		<link>https://blog.gmem.cc/go-network-programming</link>
		<comments>https://blog.gmem.cc/go-network-programming#comments</comments>
		<pubDate>Thu, 02 Feb 2017 10:10:27 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Go]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=17959</guid>
		<description><![CDATA[<p>TCP net 该包提供了网络I/O的可移植接口，支持TCP/IP、UDP、DNS查询、Unix域套接字。 尽管此包提供了访问低级网络原语，但是大部分客户端仅仅需要Dial、Listen、Accept等基本函数，以及关联的Conn、Listener接口。 客户端 Dial函数用于连接到正在监听的服务器： [crayon-69e20248bb369428155902/] 服务器  Listen函数用于创建服务器并监听端口： [crayon-69e20248bb36d233351682/] DNS 使用net包可以完成DNS查询： [crayon-69e20248bb370025723622/] HTTP net.http 此包提供了HTTP客户端和服务器的实现。 客户端 简单的客户端代码示例： [crayon-69e20248bb372925063480/] 如果需要控制请求头、重定向策略和其它设置，可以使用Client：  [crayon-69e20248bb375448868894/] 如果要控制传输相关的属性，例如代理、TLS配置、Keep-Alive、压缩，可以使用Transport：  <a class="read-more" href="https://blog.gmem.cc/go-network-programming">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-network-programming">Go语言网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">TCP</span></div>
<div class="blog_h2"><span class="graybg">net</span></div>
<p>该包提供了网络I/O的可移植接口，支持TCP/IP、UDP、DNS查询、Unix域套接字。</p>
<p>尽管此包提供了访问低级网络原语，但是大部分客户端仅仅需要Dial、Listen、Accept等基本函数，以及关联的Conn、Listener接口。</p>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>Dial函数用于连接到正在监听的服务器：</p>
<pre class="crayon-plain-tag">// 发起TCP连接
conn, err := net.Dial("tcp", "hongkong.gmem.cc:80")
if err == nil {
    // Conn是Writer，支持写入
    fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
    // Conn还是Reader，下面读取直到遇到第一个换行
    bs := bufio.NewScanner(conn)
    for bs.Scan() {
        fmt.Println(bs.Text())
    }
}</pre>
<div class="blog_h3"><span class="graybg">服务器 </span></div>
<p>Listen函数用于创建服务器并监听端口：</p>
<pre class="crayon-plain-tag">import (
    "net"
    "bufio"
    "fmt"
    "time"
)

func handleConn(conn net.Conn) {
    conn.Write([]byte("欢迎\n"))
}
func Server() {
    // 返回一个Listener
    ln, _ := net.Listen("tcp", ":8080")
    for {
        // 每接收到一个连接，就交给子Goroutine处理
        conn, _ := ln.Accept()
        go handleConn(conn)
    }
}
func Client() {
    conn, _ := net.Dial("tcp", "localhost:8080")
    bs := bufio.NewScanner(conn)
    bs.Split(bufio.ScanLines)
    bs.Scan()
    msg := bs.Text()
    conn.Close()
    fmt.Println(msg)
}
func main() {
    go Server()
    for i := 0; i &lt; 10; i++ {
        go Client()
    }
    time.Sleep(time.Second)
}</pre>
<div class="blog_h1"><span class="graybg">DNS</span></div>
<p><span class="graybg">使用net包可以完成DNS查询：</span></p>
<pre class="crayon-plain-tag">addrs, _ := net.LookupHost("tk.gmem.cc")
for _, addr := range addrs {
    println(addr) // 打印IP地址
}
names, _ := net.LookupAddr("108.61.247.199")
for _, name := range names {
    println(name) // 打印反查得到的域名
}</pre>
<div class="blog_h1"><span class="graybg">HTTP</span></div>
<div class="blog_h2"><span class="graybg">net.http</span></div>
<p>此包提供了HTTP客户端和服务器的实现。</p>
<div class="blog_h3"><span class="graybg">客户端</span></div>
<p>简单的客户端代码示例：</p>
<pre class="crayon-plain-tag">// GET请求
resp, err := http.Get("http://tokyo.gmem.cc")
if err == nil {
    println(resp.StatusCode)
    body, _ := ioutil.ReadAll(resp.Body)
    println(string(body))
}

// 通过POST提交表单
resp, _ := http.PostForm("http://tokyo.gmem.cc/article/new", url.Values{
    "name": {"Alex"},
    "id":   "1",
})

// 通过POST上传文件
resp, _ := http.Post("http://tokyo.gmem.cc/avatar/upload", "image/jpeg", &amp;imgBuf)

// 客户端必须负责关闭响应体，且必须判断resp是否为nil
if resp != nil {
    // 读取并丢弃剩余的响应主体数据。确保在keepalive http连接行为开启的情况下，可以被另一个请求复用
    defer resp.Body.Close()
}

// 标准http库默认只在HTTP服务器要求关闭时才会关闭网络连接。要在请求完成后立即关闭连接，使用下面的头
req.Header.Add("Connection", "close")

// 禁用HTTP keep-alive。在向大量服务器发送少量请求时可以禁用
tr := &amp;http.Transport{DisableKeepAlives: true}
client := &amp;http.Client{Transport: tr}</pre>
<p>如果需要控制请求头、重定向策略和其它设置，可以使用Client： </p>
<pre class="crayon-plain-tag">// 重定向策略               新请求        已经被重定向的请求，最老的为第一个元素
// 默认策略是允许10次重定向
redirectPolicy := func(req *http.Request, via []*http.Request) error {
    return nil
}

hc := &amp;http.Client{CheckRedirect: redirectPolicy}
// 创建一个请求对象
req, _ := http.NewRequest("GET", "http://tk.gmem.cc", nil)
// 设置请求头
req.Header.Add("Accept", "text/html")
// 执行请求
resp, _ := hc.Do(req)</pre>
<p>如果要控制传输相关的属性，例如代理、TLS配置、Keep-Alive、压缩，可以使用Transport： </p>
<pre class="crayon-plain-tag">tr := &amp;http.Transport{
    // 最大空闲的TCP连接数
    MaxIdleConns: 10,
    // 空闲超时时间
    IdleConnTimeout: 30 * time.Second,
    // 是否禁用压缩
    DisableCompression: true,
}
client := &amp;http.Client{Transport: tr}
resp, _ := client.Get("https://tk.gmem.cc")</pre>
<p>进行超时控制：</p>
<pre class="crayon-plain-tag">req, err := http.NewRequest("GET", u.String(), nil)
// 超时自动取消的上下文               // 默认是后台上下文
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
defer cancel()
// 使用超时上下文进行请求
r, err := http.DefaultClient.Do(req.WithContext(ctx))
defer r.Body.Close()
b, err := ioutil.ReadAll(r.Body)</pre>
<p>如果服务器使用不受信任证书，可以跳过证书校验：</p>
<pre class="crayon-plain-tag">http.DefaultTransport.(*http.Transport).TLSClientConfig = &amp;tls.Config{InsecureSkipVerify: true} </pre>
<div class="blog_h3"><span class="graybg">服务器</span></div>
<p>创建简单的服务器：</p>
<pre class="crayon-plain-tag">// 为DefaultServeMux添加Handler
http.HandleFunc("/greetings", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello %v", r.URL.Query().Get("name"))
});

// handle为nil则使用DefaultServeMux
http.ListenAndServe(":8800", nil)

wg := sync.WaitGroup{}
wg.Add(1)
wg.Wait()</pre>
<p>如果需要定制各项参数，可以使用Server： </p>
<pre class="crayon-plain-tag">type reqHandler int

func (rh reqHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // 从URL抽取请求参数
    values, ok := r.URL.Query()["key"]
    // 读取Cookie
    cookie, err := r.Cookie("token")
    // 抽取表单参数
    r.ParseForm()
    // 读取单个参数
    value := r.Form["key"][0]
    // 读取全部参数
    var params map[stirng][]string = r.PostForm

    // 读取请求体中的JSON并反序列化
    decoder := json.NewDecoder(r.Body)
    var user User
    err := decoder.Decode(&amp;user)
    // 关闭请求体
    defer req.Body.Close() 
}

func main() {
    s := &amp;http.Server{
        Addr:           ":8080",
        Handler:        reqHandler(0),
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 &lt;&lt; 20,
    }
    s.ListenAndServe()
}</pre>
<div class="blog_h3"><span class="graybg">多路器</span></div>
<p>要实现服务器端的请求处理多路器（multiplexer），参考下面的代码：</p>
<pre class="crayon-plain-tag">mux := http.DefaultServeMux
// 每个路径都必须由http.Handler处理
mux.Handle("/metrics", promhttp.Handler())
// 处理器示例
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
	// 写响应头
	w.WriteHeader(http.StatusOK)
	// 写响应体
	w.Write([]byte("OK"))
})
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	// 读取请求体
	body, err := ioutil.ReadAll(r.Body)
	defer r.Body.Close()
	// 解析为对象
	payload := &amp;flaggerv1.CanaryWebhookPayload{}
	err = json.Unmarshal(body, payload)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	w.WriteHeader(http.StatusAccepted)
})
// 创建HTTP服务器
srv := &amp;http.Server{
	Addr:         ":" + port,
	Handler:      mux,
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 1 * time.Minute,
	IdleTimeout:  15 * time.Second,
}

// 在后台启动服务器
go func() {
	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
		logger.Fatalf("HTTP server crashed %v", err)
	}
}()

// 等待信号
&lt;-stopCh

// 时限内关闭服务器
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

if err := srv.Shutdown(ctx); err != nil {
	logger.Errorf("HTTP server graceful shutdown failed %v", err)
} else {
	logger.Info("HTTP server stopped")
}</pre>
<div class="blog_h2"><span class="graybg">net.url</span></div>
<p>此包支持对URL进行操控：</p>
<pre class="crayon-plain-tag">// 解析绝对URL
promURL, err := url.Parse("http://prometheus.istio-system.svc.k8s.gmem.cc:9090")
// 解析相对URL
u, err := url.Parse("./api/v1/status/flags")
// 组合
u = promURL.ResolveReference(u)</pre>
<div class="blog_h2"><span class="graybg">imroc/req</span></div>
<p>更加人性化、简单的HTTP客户端，执行下面的命令安装：</p>
<pre class="crayon-plain-tag">go get github.com/imroc/req</pre>
<div class="blog_h3"><span class="graybg">发送请求</span></div>
<pre class="crayon-plain-tag">// 先创建请求对象，再发请求
r := req.New()
r.Get(url)

// 或者直接调用函数
req.Get(url)</pre>
<div class="blog_h3"><span class="graybg">写请求头/参数</span> </div>
<pre class="crayon-plain-tag">header := req.Header{
	"Accept":        "application/json",
	"Authorization": "Basic YWRtaW46YWRtaW4=",
}

param := req.Param{
	"name": "imroc",
	"cmd":  "add",
}

r, err = req.Post("http://foo.bar/api", header, param)</pre>
<div class="blog_h3"><span class="graybg">写请求体</span></div>
<pre class="crayon-plain-tag">req.Post(url, req.BodyJSON(&amp;foo))
req.Post(url, req.BodyXML(&amp;bar))</pre>
<div class="blog_h3"><span class="graybg">读响应头</span></div>
<pre class="crayon-plain-tag">res, err = req.Post("http://foo.bar/api", header, param)
res.Response().Header.Get(headers.ContentType)</pre>
<div class="blog_h3"><span class="graybg">读响应体</span></div>
<pre class="crayon-plain-tag">// 按JSON来解析，绑定到对象
r.ToJSON(&amp;buf) 
// 按XML来解析，绑定到对象
r.ToXML(&amp;baz)</pre>
<div class="blog_h3"><span class="graybg">操控Cookie</span></div>
<pre class="crayon-plain-tag">// 禁用Cookie
req.EnableCookie(false)


cookie := new(http.Cookie)
// 发送Cookie
req.Get(url, cookie)</pre>
<div class="blog_h3"><span class="graybg">文件上传</span></div>
<pre class="crayon-plain-tag">req.Post(url, req.File("imroc.png"), req.File("/Users/roc/Pictures/*.png"))


// 细粒度控制
file, _ := os.Open("imroc.png")
req.Post(url, req.FileUpload{
	File:      file,
	FieldName: "file",       // 表单字段名
	FileName:  "avatar.png", // 上传时使用的文件名
})

// 监听上传进度
progress := func(current, total int64) {
	fmt.Println(float32(current)/float32(total)*100, "%")
}
req.Post(url, req.File("/Users/roc/Pictures/*.png"), req.UploadProgress(progress))
fmt.Println("upload complete")</pre>
<div class="blog_h3"><span class="graybg">文件下载 </span></div>
<pre class="crayon-plain-tag">r, _ := req.Get(url)
r.ToFile("imroc.png")


// 监听下载进度
progress := func(current, total int64) {
	fmt.Println(float32(current)/float32(total)*100, "%")
}
r, _ := req.Get(url, req.DownloadProgress(progress))
r.ToFile("hello.mp4")
fmt.Println("download complete")</pre>
<div class="blog_h3"><span class="graybg">使用代理</span></div>
<p>默认情况下，环境变量http_proxy、https_proxy自动作为代理服务器。你也可以设置自己的代理服务器：</p>
<pre class="crayon-plain-tag">req.SetProxyUrl("http://my.proxy.com:23456")</pre>
<div class="blog_h3"><span class="graybg">超时控制 </span></div>
<pre class="crayon-plain-tag">req.SetTimeout(50 * time.Second)</pre>
<div class="blog_h3"><span class="graybg">定制http.Client</span></div>
<pre class="crayon-plain-tag">client := &amp;http.Client{Timeout: 30 * time.Second}
req.Get(url, client)

req.SetClient(client) </pre>
<div class="blog_h2"><span class="graybg">Web框架</span></div>
<div class="blog_h3"><span class="graybg">Gorilla</span></div>
<p>Gorilla是一个Web工具箱，提供了若干个包，具体参考<a href="/gorilla-study-note">Gorilla学习笔记</a>。</p>
<div class="blog_h3"><span class="graybg">Gin</span></div>
<p>Gin是目前Star数最高的Go语言的Web框架，具体参考<a href="/gin-study-note">Gin学习笔记</a>。</p>
<div class="blog_h3"><span class="graybg">go-restful</span></div>
<p>用于构建REST-style的WebService。支持GET、POST、PUT、DELETE、PATCH（更新资源的部分内容）、OPTIONS（获取目标URI的通信选项）等方法。</p>
<p>用法：</p>
<pre class="crayon-plain-tag">import (
	restful "github.com/emicklei/go-restful/v3"
)

// Restful容器，其中可以注册多个WebService
// 默认容器：restful.DefaultContainer
//           DefaultContainer = NewContainer()
//           DefaultContainer.ServeMux = http.DefaultServeMux
//
c := restful.NewContainer()
c.ServeMux = http.NewServeMux()
c.Router(restful.CurlyRouter{})
c.RecoverHandler(func(panicReason interface{}, httpWriter http.ResponseWriter) {
	logStackOnRecover(s, panicReason, httpWriter)
})
c.ServiceErrorHandler(func(serviceErr restful.ServiceError, request *restful.Request, response *restful.Response) {
	serviceErrorHandler(s, serviceErr, request, response)
})


// WebService，需要添加到Restful容器中
ws := new(restful.WebService)
ws.
	Path("/users").
	Consumes(restful.MIME_XML, restful.MIME_JSON).
	Produces(restful.MIME_JSON, restful.MIME_XML)

c.Add(u.WebService())

http.ListenAndServe(":8080", c.ServeMux)</pre>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg">gRPC</span></div>
<p>请参考<a href="/grpc-study-note">gRPC学习笔记</a>。</p>
<div class="blog_h1"><span class="graybg">SSH</span></div>
<p>包golang.org/x/crypto/ssh提供了SSH协议的支持：</p>
<pre class="crayon-plain-tag">func main() {
    sess, err := connect("root", "lavender", "xenial-100", 22)
    if err != nil {
        log.Fatalf(err.Error())
    }
    // 将SSH会话的标准输出/错误重定向到当前应用的
    sess.Stdout = os.Stdout
    sess.Stderr = os.Stderr

    // 执行命令
    sess.Run("uname -a")
    
    // 执行命令，并获取其标准输出
    result, _ := sess.Output("uname -a")
    fmt.Printf("%s", result)



    // 要执行多个命令，可以
    stdinBuf, _ := ssh.StdinPipe()
    sess.Shell()
    stdinBuf.Write([]byte("ls"))
    stdinBuf.Write([]byte("\n"))
    stdinBuf.Write([]byte("uname"))
    stdinBuf.Write([]byte("\n"))
    
    // 需要调用exit，让会话退出，否则Wait()调用永久阻塞
    stdinBuf.Write([]byte("exit\n"))
    // 需要调用此函数，否则可能过早退出，命令却没有执行
    ssh.Wait()

    sess.Close()
}
func connect(user, password, host string, port int) (*ssh.Session, error) {
    var (
        auth         []ssh.AuthMethod
        addr         string
        clientConfig *ssh.ClientConfig
        client       *ssh.Client
        err          error
    )
    auth = make([]ssh.AuthMethod, 0)
    // 身份认证方法，支持密码或sshkey
    auth = append(auth, ssh.Password(password))

    clientConfig = &amp;ssh.ClientConfig{
        User:            user,
        Auth:            auth,
        Timeout:         30 * time.Second,
        // 允许任意的服务器公钥
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    addr = fmt.Sprintf("%s:%d", host, port)
    // 发起连接
    if client, err = ssh.Dial("tcp", addr, clientConfig); err != nil {
        return nil, err
    }

    // 创建会话
    return client.NewSession()
}</pre>
<div class="blog_h1"><span class="graybg">连接复用</span></div>
<p><a href="https://github.com/soheilhy/cmux">cmux</a>是一个连接复用器，允许在同一个端口上提供不同类型的服务 —— 包括gRPC、SSH、HTTPS、HTTP、Go RPC，等等。</p>
<p>cmux仅仅需要检测一个连接的最开始几个字节，因此对<span style="background-color: #c0c0c0;">性能的影响是微不足道</span>的。</p>
<p>注意点：</p>
<ol>
<li>关于TLS：包net/http基于断言来识别TLS连接，由于cmux使用 lookahead-implementing的连接来装饰底层TCP连接，导致net/http的断言会失败。后果是，你可以使用cmux来服务HTTPS，但是不会为你的Handler设置http.Request.TLS</li>
<li>一个连接，自始自终必须使用同一协议。也就是说，一个Connection要么使用gRPC，要么使用REST，而不能随意切换</li>
<li>关于Java的gRPC客户端：此客户端会在接收到服务器返回的SETTINGS帧之前一直阻塞，你应当使用下面的代码：<br />
<pre class="crayon-plain-tag">grpcl := m.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">示例</span></div>
<pre class="crayon-plain-tag">import "github.com/soheilhy/cmux"

// 创建主监听器
l, err := net.Listen("tcp", ":80")
if err != nil {
	log.Fatal(err)
}

// 为监听器创建cmux
m := cmux.New(l)

// 按照声明顺序，逐个去匹配协议
// 匹配gRPC
grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
// 匹配HTTP
httpL := m.Match(cmux.HTTP1Fast())
// 所有不匹配的其它连接请求，都看作Go RPC/TCP
trpcL := m.Match(cmux.Any())


// 为不同协议创建服务器
//gRPC
grpcS := grpc.NewServer()
grpchello.RegisterGreeterServer(grpcS, &amp;server{})
// HTTP
httpS := &amp;http.Server{
	Handler: &amp;helloHTTP1Handler{},
}
// Go RPC/TCP
trpcS := rpc.NewServer()
trpcS.Register(&amp;ExampleRPCRcvr{})

// 将各协议的服务器注册到muxed的监听器
go grpcS.Serve(grpcL)
go httpS.Serve(httpL)
go trpcS.Accept(trpcL)

// 开始服务
m.Serve()</pre>
<p>&nbsp;</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/go-network-programming">Go语言网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/go-network-programming/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux网络编程</title>
		<link>https://blog.gmem.cc/linux-network-programming</link>
		<comments>https://blog.gmem.cc/linux-network-programming#comments</comments>
		<pubDate>Thu, 18 Jun 2009 11:53:21 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[C]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">https://blog.gmem.cc/?p=25263</guid>
		<description><![CDATA[<p>系统编程接口 错误码 代码 名字 说明 0   Success 1 EPERM Operation not permitted 2 ENOENT No such file or directory 3 ESRCH <a class="read-more" href="https://blog.gmem.cc/linux-network-programming">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-network-programming">Linux网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">系统编程接口</span></div>
<div class="blog_h2"><span class="graybg"><a id="skt-enos"></a>错误码</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 50px; text-align: center;">代码</td>
<td style="width: 100px; text-align: center;">名字</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td> </td>
<td>Success</td>
</tr>
<tr>
<td>1</td>
<td>EPERM</td>
<td>Operation not permitted</td>
</tr>
<tr>
<td>2</td>
<td>ENOENT</td>
<td>No such file or directory</td>
</tr>
<tr>
<td>3</td>
<td>ESRCH</td>
<td>No such process</td>
</tr>
<tr>
<td>4</td>
<td>EINTR</td>
<td>Interrupted system call</td>
</tr>
<tr>
<td>5</td>
<td>EIO</td>
<td>Input/output error</td>
</tr>
<tr>
<td>6</td>
<td>ENXIO</td>
<td>No such device or address</td>
</tr>
<tr>
<td>7</td>
<td>E2BIG</td>
<td>Argument list too long</td>
</tr>
<tr>
<td>8</td>
<td>ENOEXEC</td>
<td>Exec format error</td>
</tr>
<tr>
<td>9</td>
<td>EBADF</td>
<td>Bad file descriptor</td>
</tr>
<tr>
<td>10</td>
<td>ECHILD</td>
<td>No child processes</td>
</tr>
<tr>
<td>11</td>
<td>EAGAIN</td>
<td>
<p>Try again  / Resource temporarily unavailable</p>
<p>提示操作将被阻塞，但是请求了非阻塞操作</p>
<p>对于send()，可能原因包括：</p>
<ol>
<li>使用<pre class="crayon-plain-tag">fcntl()</pre>显式的将文件描述符标记为非阻塞的</li>
<li>传递<pre class="crayon-plain-tag">MSG_DONTWAIT</pre>标记给<pre class="crayon-plain-tag">send()</pre></li>
<li>使用套接字选项<pre class="crayon-plain-tag">SO_SNDTIMEO</pre>设置了发送超时</li>
<li>发送的消息无法放入（满了）套接字缓冲时，send()默认会阻塞。但是如果套接字被设置为non-blocking I/O模式，则会返回EAGAIN</li>
</ol>
</td>
</tr>
<tr>
<td>12</td>
<td>ENOMEM</td>
<td>Cannot allocate memory</td>
</tr>
<tr>
<td>13</td>
<td>EACCES</td>
<td>Permission denied</td>
</tr>
<tr>
<td>14</td>
<td>EFAULT</td>
<td>Bad address</td>
</tr>
<tr>
<td>15</td>
<td>ENOTBLK</td>
<td>Block device required</td>
</tr>
<tr>
<td>16</td>
<td>EBUSY</td>
<td>Device or resource busy</td>
</tr>
<tr>
<td>17</td>
<td>EEXIST</td>
<td>File exists</td>
</tr>
<tr>
<td>18</td>
<td>EXDEV</td>
<td>Invalid cross-device link</td>
</tr>
<tr>
<td>19</td>
<td>ENODEV</td>
<td>No such device</td>
</tr>
<tr>
<td>20</td>
<td>ENOTDIR</td>
<td>Not a directory</td>
</tr>
<tr>
<td>21</td>
<td>EISDIR</td>
<td>Is a directory</td>
</tr>
<tr>
<td>22</td>
<td>EINVAL</td>
<td>Invalid argument</td>
</tr>
<tr>
<td>23</td>
<td>ENFILE</td>
<td>Too many open files in system</td>
</tr>
<tr>
<td>24</td>
<td>EMFILE</td>
<td>Too many open files</td>
</tr>
<tr>
<td>25</td>
<td>ENOTTY</td>
<td>Inappropriate ioctl for device</td>
</tr>
<tr>
<td>26</td>
<td>ETXTBSY</td>
<td>Text file busy</td>
</tr>
<tr>
<td>27</td>
<td>EFBIG</td>
<td>File too large</td>
</tr>
<tr>
<td>28</td>
<td>ENOSPC</td>
<td>No space left on device</td>
</tr>
<tr>
<td>29</td>
<td>ESPIPE</td>
<td>Illegal seek</td>
</tr>
<tr>
<td>30</td>
<td>EROFS</td>
<td>Read-only file system</td>
</tr>
<tr>
<td>31</td>
<td>EMLINK</td>
<td>Too many links</td>
</tr>
<tr>
<td>32</td>
<td>EPIPE</td>
<td>Broken pipe</td>
</tr>
<tr>
<td>33</td>
<td>EDOM</td>
<td>Numerical argument out of domain</td>
</tr>
<tr>
<td>34</td>
<td>ERANGE</td>
<td>Numerical result out of range</td>
</tr>
<tr>
<td>35</td>
<td> </td>
<td>Resource deadlock avoided</td>
</tr>
<tr>
<td>36</td>
<td> </td>
<td>File name too long</td>
</tr>
<tr>
<td>37</td>
<td> </td>
<td>No locks available</td>
</tr>
<tr>
<td>38</td>
<td> </td>
<td>Function not implemented</td>
</tr>
<tr>
<td>39</td>
<td> </td>
<td>Directory not empty</td>
</tr>
<tr>
<td>40</td>
<td> </td>
<td>Too many levels of symbolic links</td>
</tr>
<tr>
<td>41</td>
<td> </td>
<td>Unknown error 41</td>
</tr>
<tr>
<td>42</td>
<td> </td>
<td>No message of desired type</td>
</tr>
<tr>
<td>43</td>
<td> </td>
<td>Identifier removed</td>
</tr>
<tr>
<td>44</td>
<td> </td>
<td>Channel number out of range</td>
</tr>
<tr>
<td>45</td>
<td> </td>
<td>Level 2 not synchronized</td>
</tr>
<tr>
<td>46</td>
<td> </td>
<td>Level 3 halted</td>
</tr>
<tr>
<td>47</td>
<td> </td>
<td>Level 3 reset</td>
</tr>
<tr>
<td>48</td>
<td> </td>
<td>Link number out of range</td>
</tr>
<tr>
<td>49</td>
<td> </td>
<td>Protocol driver not attached</td>
</tr>
<tr>
<td>50</td>
<td> </td>
<td>No CSI structure available</td>
</tr>
<tr>
<td>51</td>
<td> </td>
<td>Level 2 halted</td>
</tr>
<tr>
<td>52</td>
<td> </td>
<td>Invalid exchange</td>
</tr>
<tr>
<td>53</td>
<td> </td>
<td>Invalid request descriptor</td>
</tr>
<tr>
<td>54</td>
<td> </td>
<td>Exchange full</td>
</tr>
<tr>
<td>55</td>
<td> </td>
<td>No anode</td>
</tr>
<tr>
<td>56</td>
<td> </td>
<td>Invalid request code</td>
</tr>
<tr>
<td>57</td>
<td> </td>
<td>Invalid slot</td>
</tr>
<tr>
<td>58</td>
<td> </td>
<td>Unknown error 58</td>
</tr>
<tr>
<td>59</td>
<td> </td>
<td>Bad font file format</td>
</tr>
<tr>
<td>60</td>
<td> </td>
<td>Device not a stream</td>
</tr>
<tr>
<td>61</td>
<td> </td>
<td>No data available</td>
</tr>
<tr>
<td>62</td>
<td> </td>
<td>Timer expired</td>
</tr>
<tr>
<td>63</td>
<td> </td>
<td>Out of streams resources</td>
</tr>
<tr>
<td>64</td>
<td> </td>
<td>Machine is not on the network</td>
</tr>
<tr>
<td>65</td>
<td> </td>
<td>Package not installed</td>
</tr>
<tr>
<td>66</td>
<td> </td>
<td>Object is remote</td>
</tr>
<tr>
<td>67</td>
<td> </td>
<td>Link has been severed</td>
</tr>
<tr>
<td>68</td>
<td> </td>
<td>Advertise error</td>
</tr>
<tr>
<td>69</td>
<td> </td>
<td>Srmount error</td>
</tr>
<tr>
<td>70</td>
<td> </td>
<td>Communication error on send</td>
</tr>
<tr>
<td>71</td>
<td> </td>
<td>Protocol error</td>
</tr>
<tr>
<td>72</td>
<td> </td>
<td>Multihop attempted</td>
</tr>
<tr>
<td>73</td>
<td> </td>
<td>RFS specific error</td>
</tr>
<tr>
<td>74</td>
<td> </td>
<td>Bad message</td>
</tr>
<tr>
<td>75</td>
<td> </td>
<td>Value too large for defined data type</td>
</tr>
<tr>
<td>76</td>
<td> </td>
<td>Name not unique on network</td>
</tr>
<tr>
<td>77</td>
<td> </td>
<td>File descriptor in bad state</td>
</tr>
<tr>
<td>78</td>
<td> </td>
<td>Remote address changed</td>
</tr>
<tr>
<td>79</td>
<td> </td>
<td>Can not access a needed shared library</td>
</tr>
<tr>
<td>80</td>
<td> </td>
<td>Accessing a corrupted shared library</td>
</tr>
<tr>
<td>81</td>
<td> </td>
<td>.lib section in a.out corrupted</td>
</tr>
<tr>
<td>82</td>
<td> </td>
<td>Attempting to link in too many shared libraries</td>
</tr>
<tr>
<td>83</td>
<td> </td>
<td>Cannot exec a shared library directly</td>
</tr>
<tr>
<td>84</td>
<td> </td>
<td>Invalid or incomplete multibyte or wide character</td>
</tr>
<tr>
<td>85</td>
<td> </td>
<td>Interrupted system call should be restarted</td>
</tr>
<tr>
<td>86</td>
<td> </td>
<td>Streams pipe error</td>
</tr>
<tr>
<td>87</td>
<td> </td>
<td>Too many users</td>
</tr>
<tr>
<td>88</td>
<td> </td>
<td>Socket operation on non-socket</td>
</tr>
<tr>
<td>89</td>
<td> </td>
<td>Destination address required</td>
</tr>
<tr>
<td>90</td>
<td> </td>
<td>Message too long</td>
</tr>
<tr>
<td>91</td>
<td> </td>
<td>Protocol wrong type for socket</td>
</tr>
<tr>
<td>92</td>
<td> </td>
<td>Protocol not available</td>
</tr>
<tr>
<td>93</td>
<td> </td>
<td>Protocol not supported</td>
</tr>
<tr>
<td>94</td>
<td> </td>
<td>Socket type not supported</td>
</tr>
<tr>
<td>95</td>
<td> </td>
<td>Operation not supported</td>
</tr>
<tr>
<td>96</td>
<td> </td>
<td>Protocol family not supported</td>
</tr>
<tr>
<td>97</td>
<td> </td>
<td>Address family not supported by protocol</td>
</tr>
<tr>
<td>98</td>
<td> </td>
<td>Address already in use</td>
</tr>
<tr>
<td>99</td>
<td> </td>
<td>Cannot assign requested address</td>
</tr>
<tr>
<td>100</td>
<td> </td>
<td>Network is down</td>
</tr>
<tr>
<td>101</td>
<td> </td>
<td>Network is unreachable</td>
</tr>
<tr>
<td>102</td>
<td> </td>
<td>Network dropped connection on reset</td>
</tr>
<tr>
<td>103</td>
<td> </td>
<td>Software caused connection abort</td>
</tr>
<tr>
<td>104</td>
<td> </td>
<td>Connection reset by peer</td>
</tr>
<tr>
<td>105</td>
<td> </td>
<td>No buffer space available</td>
</tr>
<tr>
<td>106</td>
<td> </td>
<td>Transport endpoint is already connected</td>
</tr>
<tr>
<td>107</td>
<td> </td>
<td>Transport endpoint is not connected</td>
</tr>
<tr>
<td>108</td>
<td> </td>
<td>Cannot send after transport endpoint shutdown</td>
</tr>
<tr>
<td>109</td>
<td> </td>
<td>Too many references: cannot splice</td>
</tr>
<tr>
<td>110</td>
<td> </td>
<td>Connection timed out</td>
</tr>
<tr>
<td>111</td>
<td> </td>
<td>Connection refused</td>
</tr>
<tr>
<td>112</td>
<td> </td>
<td>Host is down</td>
</tr>
<tr>
<td>113</td>
<td> </td>
<td>No route to host</td>
</tr>
<tr>
<td>114</td>
<td> </td>
<td>Operation already in progress</td>
</tr>
<tr>
<td>115</td>
<td> </td>
<td>Operation now in progress</td>
</tr>
<tr>
<td>116</td>
<td> </td>
<td>Stale NFS file handle</td>
</tr>
<tr>
<td>117</td>
<td> </td>
<td>Structure needs cleaning</td>
</tr>
<tr>
<td>118</td>
<td> </td>
<td>Not a XENIX named type file</td>
</tr>
<tr>
<td>119</td>
<td> </td>
<td>No XENIX semaphores available</td>
</tr>
<tr>
<td>120</td>
<td> </td>
<td>Is a named type file</td>
</tr>
<tr>
<td>121</td>
<td> </td>
<td>Remote I/O error</td>
</tr>
<tr>
<td>122</td>
<td> </td>
<td>Disk quota exceeded</td>
</tr>
<tr>
<td>123</td>
<td> </td>
<td>No medium found</td>
</tr>
<tr>
<td>124</td>
<td> </td>
<td>Wrong medium type</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">套接字选项</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">选项</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>SO_REUSEADDR</td>
<td>
<p>当绑定源地址的时候，可以通过绑定到0.0.0.0:port来<span style="background-color: #c0c0c0;">绑定到所有本地网络地址的对应端口</span>上，也可以绑定到10.0.0.1:port来绑定到特定本地网络地址的端口</p>
<p>在默认设置下，没有socket能够绑定到同一地址的同一端口。比如在Socket A已经绑定了0.0.0.0:80以后，Socket B若是想要绑定10.0.0.0.1:80，那就会报<pre class="crayon-plain-tag">EADDRINUSE</pre>。因为Socket A已经绑定了所有ip地址的80端口，包括10.0.0.1:8000</p>
<p>如果Socket B设置了选项SO_REUSEADDR，那么：</p>
<ol>
<li>它不可以绑定0.0.0.0:80</li>
<li>它可以绑定10.0.0.0.1:80， 也就是说，除非有<span style="background-color: #c0c0c0;">Socket已经绑定到和它字面上一模一样的地址，才报错</span></li>
</ol>
<p>此外SO_REUSEADDR还可以<span style="background-color: #c0c0c0;">允许绑定<a href="/network-faq#connstate">TIME_WAIT</a>状态的连接占据的源地址</span></p>
</td>
</tr>
<tr>
<td>SO_REUSEPORT</td>
<td>允许多个Socket绑定到完全相同的IP和端口</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">套接字简介</span></div>
<p>Berkeley sockets（BSD sockets，来自BSD 4.2，1983），是一套用于操控因特网套接字、UNIX域套接字的接口标准，是进程间通信的一种方式。</p>
<p>POSIX sockets与Berkeley sockets的差别很小。大部分的现代操作系统实现了Berkeley sockets接口，甚至包括Microsoft的Winsock。</p>
<div class="blog_h3"><span class="graybg">五元组</span></div>
<p>一个套接字（连接）由五元组来标识：<pre class="crayon-plain-tag">&lt;protocol&gt;, &lt;src addr&gt;, &lt;src port&gt;, &lt;dest addr&gt;, &lt;dest port&gt;</pre></p>
<p>其中：</p>
<ol>
<li>protocol在创建<pre class="crayon-plain-tag">socket()</pre>时设置</li>
<li>src addr / src port 在<pre class="crayon-plain-tag">bind()</pre>时设置</li>
<li>dest addr / dest port在<pre class="crayon-plain-tag">connect()</pre>时设置</li>
</ol>
<p>虽然UDP不需要connect()，但是dest addr / dest port会在第一次发送数据数据时由系统隐式设置。</p>
<div class="blog_h2"><span class="graybg">套接字API</span></div>
<p>这套API主要包含了以下头文件：</p>
<table class="fixed-word-wrap full-width" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">头文件</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">sys/socket.h</td>
<td>包括BSD套接字的核心函数、数据结构</td>
</tr>
<tr>
<td class="blog_h3">netinet/in.h</td>
<td>与AF_INET、AF_INET6地址族相关的内容 </td>
</tr>
<tr>
<td class="blog_h3">sys/un.h</td>
<td>与PF_UNIX/PF_LOCAL地址族相关的内容</td>
</tr>
<tr>
<td class="blog_h3">arpa/inet.h</td>
<td>包含一些操控IP地址的函数</td>
</tr>
<tr>
<td class="blog_h3">netdb.h</td>
<td>用来转换协议名称、主机名称为数字化的地址</td>
</tr>
<tr>
<td class="blog_h3"><span style="color: #252525;">unistd.h</span></td>
<td>UNIX标准头，包含很多系统调用（fork、pipe）的封装和I/O原语，例如read、write、close</td>
</tr>
</tbody>
</table>
<p>包含的主要函数有：</p>
<table class="full-width fixed-word-wrap" style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;"> 函数</td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td class=" blog_h3">socket()</td>
<td>创建以整数（描述符）来识别的、指定类型的套接字，并为其分配系统资源<br />
<pre class="crayon-plain-tag">/**
 * 创建一个通信端点（endpoint）并返回套接字的文件描述符，如果出错返回-1
 * domain 
 *   指定协议族：AF_INET、AF_INET6、AF_UNIX
 * type 
 *   套接字类型：SOCK_STREAM、SOCK_DGRAM、SOCK_SEQPACKET、SOCK_RAW 
 * protocol 
 *   传输协议：IPPROTO_TCP、IPPROTO_SCTP、IPPROTO_UDP、IPPROTO_DCCP
 *   这些协议定义在netinet/in.h，如果指定0，则根据domain、type推导默认值
 */
int socket(int domain, int type, int protocol);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">bind()</td>
<td>一般用于服务器端，将一个socket与socket address结构体（包含本地IP、端口信息）关联<br />
<pre class="crayon-plain-tag">/**
 * 为套接字分配地址，成功返回0否则返回-1
 * sockfd
 *   被分配地址的套接字的描述符
 * my_addr
 *   代表地址的sockaddr结构的指针
 * addrlen
 *   指定sockaddr结构的长度
 */
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">listen()</td>
<td>用于服务器端，导致绑定的TCP socket进入监听（LISTENING）状态<br />
<pre class="crayon-plain-tag">/**
 * 仅用于面向流（面向连接）的套接字服务端
 * sockfd
 *   监听的套接字描述符
 * backlog
 *   排队入站请求数量，一旦请求被接受即移出队列
 *   超过队列限制的请求被直接拒绝
 */
int listen(int sockfd, int backlog);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">connect()</td>
<td>用于客户端，将本地空闲端口分配给套接字，对于TCP，该函数将尝试建立TCP连接<br />
<pre class="crayon-plain-tag">/**
 * 该系统调用通过自己指定的套接字，连接到服务器端套接字
 * 对于无连接的套接字类型，该调用不建立连接，只是用于说明数据报的默认目标
 * sockfd
 *   本地套接字描述符
 * serv_addr
 *   远程套接字监听地址
 * addrlen
 *   远程套接字地址的长度  
 */
int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">accept()</td>
<td>用于服务器端，接受一个入站连接请求，创建并返回关联了socket pair的socket<br />
<pre class="crayon-plain-tag">/**
 * 为连接请求创建一个新的套接字，并移出队列
 * sockfd
 *   监听套接字的描述符
 * cliaddr
 *   用于接收客户端地址信息的结构
 * addrlen
 *   sockaddr类型的长度
 */
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">send()</td>
<td>TCP Socket的send()是一个异步调用，当数据送入socket send buffer以后就会返回。也就是说，在send()返回以后，数据仍然需要经历TCP拥塞控制等过程，才能被成功发送</td>
</tr>
<tr>
<td class=" blog_h3">recv()</td>
<td>read返回值：0表示对端关闭了连接；-1表示发生错误</td>
</tr>
<tr>
<td class=" blog_h3">write()</td>
<td> </td>
</tr>
<tr>
<td class=" blog_h3">read()</td>
<td> </td>
</tr>
<tr>
<td class=" blog_h3">sendto()</td>
<td>用于UDP场景下的发送数据</td>
</tr>
<tr>
<td class=" blog_h3">recvfrom()</td>
<td>用于UDP场景下的接收数据</td>
</tr>
<tr>
<td class=" blog_h3">close()</td>
<td>
<p>关闭套接字的输入/输出通道，导致系统释放与套接字相关的资源，对于TCP，连接被关闭。对于客户端，即使connect()失败也要关闭套接字</p>
<p>该调用仅仅销毁了套接字的接口，套接字本身由OS内核负责销毁，某些情况下，套接字可能进入TIME_WAIT状态</p>
</td>
</tr>
<tr>
<td class=" blog_h3">shutdown()</td>
<td>
<p>类似于close()，但是可以实现“半关闭”，即只关闭输出通道或者输入通道</p>
</td>
</tr>
<tr>
<td class=" blog_h3">gethostbyname()</td>
<td>用于解析IPv4的主机名、IP地址<br />
<pre class="crayon-plain-tag">/**
 * 通过DNS系统或者/etc/hosts来查找主机名对应的Internet地址信息
 * 如果调用成功返回代表因特网地址的结构hostent，否则返回NULL指针
 * 出错时通过h_errno可以检查具体原因
 * name
 *   主机名，例如gmem.cc
 */
struct hostent *gethostbyname(const char *name);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">gethostbyaddr()</td>
<td>
<pre class="crayon-plain-tag">/**
 * 类似于上面的函数，通过地址来查询
 * addr
 *   in_addr类型的指针，代表了主机的地址
 * len
 *   上面地址的长度
 * type
 *   地址族类型，例如AF_INET
 */
struct hostent *gethostbyaddr(const void *addr, int len, int type);</pre>
</td>
</tr>
<tr>
<td class=" blog_h3">select()</td>
<td>
<p>轮询，等待所提供列表中一个或者多个套接字可读、可写或者有错误发生。具有以下缺点：</p>
<ol>
<li>每次调用时，列表中所有套接字的文件描述符需要拷贝到内核空间</li>
<li>每次调用时，需要在内核态遍历文件描述符</li>
<li>支持的文件描述符数量较小，默认1024</li>
</ol>
</td>
</tr>
<tr>
<td class=" blog_h3">poll()</td>
<td>与select()类似，但是描述文件描述符的方式不同，select()使用fd_set结构，而poll()使用pollfd结构</td>
</tr>
<tr>
<td class=" blog_h3">epoll()</td>
<td>Linux内核为处理大批量文件描述符而作了改进的poll</td>
</tr>
<tr>
<td class=" blog_h3">getsockopt()</td>
<td>获取某个套接字选项当前的值</td>
</tr>
<tr>
<td class=" blog_h3">setsockopt()</td>
<td>设置某个套接字选项的值</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">Linux代码示例</span></div>
<div class="blog_h3"><span class="graybg">Echo示例 </span></div>
<pre class="crayon-plain-tag">#include &lt;stdio.h&gt;
#include &lt;string.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;arpa/inet.h&gt;
#include &lt;unistd.h&gt;

#define DEFAULT_PORT 1918

const char* ip2txt( struct sockaddr_in addr, char* buf );

int main( void )
{
    int sock_desc, client_sock, read_size;
    struct sockaddr_in server, client;
    socklen_t socklen = sizeof ( server );
    char client_message[2000], ipv4_buf[INET_ADDRSTRLEN];
    struct timeval timeout;
    timeout.tv_sec = 3;
    timeout.tv_usec = 0;

    setvbuf( stdout, NULL, _IONBF, 0 );

    //创建套接字，返回一个描述符
    sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    if ( sock_desc == -1 )
    {
        puts( "Failed to create socket" );
        return 1;
    }
    //监听套接字结构
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = INADDR_ANY;
    server.sin_port = htons( DEFAULT_PORT );
    //绑定本地监听地址到描述符
    if ( bind( sock_desc, ( struct sockaddr * ) &amp;server, socklen ) &lt; 0 )
    {
        puts( "Binding failed" );
        return 1;
    }
    else
    {
        printf( "Binding to local socket address: %s:%d\n", ip2txt( server, ipv4_buf ), DEFAULT_PORT );
    }
    //开始监听，等待队列容量10
    listen( sock_desc, 10 );
    for ( ;; )
    {
        //接收一个客户端连接，返回代表此套接字的描述符
        client_sock = accept( sock_desc, ( struct sockaddr * ) &amp;client, &amp;socklen );
        printf( "Connection from %s:%d accepted\n", ip2txt( client, ipv4_buf ), ntohs( client.sin_port ) );
        //设置读取超时，此方法用于Linux，Cygwin无效
        if ( setsockopt( client_sock, SOL_SOCKET, SO_RCVTIMEO, &amp;timeout, sizeof(struct timeval) ) == 0 )
        {
            printf( "recv() timeout set to %ds\n", ( int ) timeout.tv_sec );
        }
        //循环读取数据，该方法会立即返回
        while ( ( read_size = recv( client_sock, client_message, sizeof ( client_message ), 0 ) ) &gt; 0 )
        {
            //把数据原样写给客户端
            printf( "Received message: %s\n", client_message );
            write( client_sock, client_message, strlen( client_message ) );
        }

        //如果超时后仍然读取不到数据、或者出现其他错误，会返回-1
        puts( "Error recv()." );
    }
    return 0;
}

const char* ip2txt( struct sockaddr_in addr, char* buf )
{
    //该函数用来将网络字节序的整数转换为点号分隔的IP地址格式
    return inet_ntop( AF_INET, &amp;addr.sin_addr, buf, INET_ADDRSTRLEN );
}</pre>
<p>客户端代码示例：</p>
<pre class="crayon-plain-tag">void client()
{
    int client_sock_desc;
    struct sockaddr_in server;
    char client_message[2000], server_message[2000];
    client_sock_desc = socket( AF_INET, SOCK_STREAM, 0 );
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr( "192.168.0.90" );
    server.sin_port = htons( DEFAULT_PORT );
    socklen_t socklen = sizeof ( server );
    //连接到服务器端
    if ( connect( client_sock_desc, ( struct sockaddr* ) &amp;server, socklen ) &lt; 0 )
    {
        printf( "Connect failed.\n" );
        return;
    }
    while ( 1 )
    {
        printf( "Enter message: \n" );
        scanf( "%s", client_message );
        //发送消息
        if ( send( client_sock_desc, client_message, strlen( client_message ), 0 ) &lt; 0 )
        {
            printf( "Send failed.\n" );
            return;
        }
        //接收回应
        if ( recv( client_sock_desc, server_message, sizeof ( server_message ), 0 ) &lt; 0 )
        {
            printf( "Recv failed.\n" );
            return;
        }
        printf( "Message from server:%s\n", server_message );
    }
}</pre>
<div class="blog_h3"><span class="graybg">多进程服务器示例</span></div>
<p>一般情况下，对于网络服务器不适用“多进程”方式处理，因为网络服务器通常需要很多内存资源，fork()却会复制这些资源。这里只是做一个简单示例：</p>
<pre class="crayon-plain-tag">bind( server_sockfd, ( struct sockaddr * ) &amp;server_address, server_len );
listen( server_sockfd, 5 );
signal( SIGCHLD, SIG_IGN ); //忽略子进程的退出
while ( 1 )
{
    client_len = sizeof ( client_address );
    //接受一个连接请求
    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &amp;client_address, &amp;client_len );
    //创建子进程处理请求
    if ( fork() == 0 )
    {
        //如果是子进程，读取消息并处理
        //注意子进程继承父进程打开的文件描述符
        read( client_sockfd, &amp;ch, 1 );
        write( client_sockfd, &amp;ch, 1 );
        close( client_sockfd );
        exit( 0 );
    }
    else
    {
        //如果是父进程，只需要关闭描述符
        close( client_sockfd );
    }
}</pre>
<div class="blog_h3"><span class="graybg">基于Select的异步I/O</span></div>
<p>select系统调用允许同时在<span style="background-color: #c0c0c0;">多个底层文件描述符</span>上等待<span style="background-color: #c0c0c0;">输入的到达或输出的完成</span>，避免在单个输入输出上的忙等待。下面是相关的函数说明：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/time.h&gt;
//下面这组宏用于对文件描述符集合进行操作
void FD_ZERO( fd_set *fdset );//将文件描述符集初始化为空集合
int FD_ISSET( int fd, fd_set *fdset ); //判断文件描述符fd是否存在于fdset中
void FD_CLR( int fd, fd_set *fdset ); //清除一个文件描述符
void FD_SET( int fd, fd_set *fdset ); //设置一个文件描述符
/**
 * 测试文件描述符集中是否至少有一个文件描述符已经处于可读、可写、错误状态。
 * 该函数在以下情况下返回：
 * 1、readfds具有可读、writefds具有可写、errorfds存在描述符遇到错误条件
 * 2、如果上述三个情况都没有发生，该调用在timeout后返回
 * 当返回时，描述符集合被修改为指示哪些描述符可读、可写或者处于错误状态
 * 
 * @param nfds 需要测试的文件描述符数量，从0到nfds-1
 * 下面三个参数都可以被设置为空指针，表示不进行相应的测试
 * @param readfds  读文件描述符
 * @param writefds 写文件描述符
 * @param errorfds 错误文件描述符
 * @param timeout 超时时间，如果是空指针将一直阻塞
 * @return 状态无变化的描述符总数，失败时返回-1并设置errno：
 *         EBADF 无效文件描述符
 *         EINTR 因中断而返回
 *         EINVAL ndfs或者timeout取值错误
 */
int select( int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout );
struct timeval
{
    time_t tv_sec; /* 毫秒 */
    long tv_usec; /* 微秒 */
};</pre>
<p>下面是一段示例代码，通过单进程服务多个客户端，并将请求值加1返回给客户端：</p>
<pre class="crayon-plain-tag">#include &lt;sys/types.h&gt;
#include &lt;sys/socket.h&gt;
#include &lt;stdio.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;sys/time.h&gt;
#include &lt;sys/ioctl.h&gt;
#include &lt;unistd.h&gt;
#include &lt;stdlib.h&gt;

#define DEFAULT_PORT 1918

int main()
{
    int server_sockfd, client_sockfd;
    int server_len, client_len;
    struct sockaddr_in server_address;
    struct sockaddr_in client_address;
    int result;
    fd_set readfds, testfds;
    //创建客户端TCP套接字
    server_sockfd = socket( AF_INET, SOCK_STREAM, 0 );
    server_address.sin_family = AF_INET;
    server_address.sin_addr.s_addr = htonl( INADDR_ANY );
    server_address.sin_port = htons( DEFAULT_PORT );
    server_len = sizeof ( server_address );
    //绑定套接字描述符到本地地址
    bind( server_sockfd, ( struct sockaddr * ) &amp;server_address, server_len );
    //在套接字描述符上监听
    listen( server_sockfd, 5 );
    FD_ZERO( &amp;readfds );
    FD_SET( server_sockfd, &amp;readfds ); //将监听描述符添加到集合中
    while ( 1 )
    {
        char ch;
        int fd;
        int nread;
        testfds = readfds;
        //一直等待，直到输入（连接请求）到达
        result = select( FD_SETSIZE, &amp;testfds, ( fd_set * ) 0, ( fd_set * ) 0, ( struct timeval * ) 0 );
        for ( fd = 0; fd &lt; FD_SETSIZE; fd++ )
        {
            //遍历所有就绪的可读描述符
            if ( FD_ISSET( fd, &amp;testfds ) )
            {
                if ( fd == server_sockfd )
                {
                    //这是一个服务器监听套接字
                    //服务器端套接字就绪，说明有连接请求
                    client_len = sizeof ( client_address );
                    //生成客户套接字，并加入到读套接字集中
                    client_sockfd = accept( server_sockfd, ( struct sockaddr * ) &amp;client_address, &amp;client_len );
                    FD_SET( client_sockfd, &amp;readfds );
                }
                else
                {
                    //这是一个客户套接字
                    ioctl( fd, FIONREAD, &amp;nread ); //得到可读数据字节数
                    if ( nread == 0 )
                    {
                        close( fd );
                        FD_CLR( fd, &amp;readfds ); //从读集合中移除该客户端
                    }
                    else
                    {
                        read( fd, &amp;ch, 1 ); //读取一个字节
                        ch++;
                        write( fd, &amp;ch, 1 ); //写回一个字节
                    }
                }
            }
        }
    }
}</pre>
<div class="blog_h3"><span class="graybg">UDP示例</span></div>
<pre class="crayon-plain-tag">sockfd = socket( AF_INET, SOCK_DGRAM, 0 );
address.sin_family = AF_INET;
address.sin_port = servinfo-&gt;s_port;
address.sin_addr = *( struct in_addr * ) *hostinfo-&gt;h_addr_list;
len = sizeof(address);
//向address发送一个数据报
sendto( sockfd, buffer, 1, 0, ( struct sockaddr * ) &amp;address, len );
//从address接收数据报
recvfrom( sockfd, buffer, sizeof(buffer), 0, ( struct sockaddr * ) &amp;address, &amp;len );</pre>
<div class="blog_h3"><span class="graybg">TCP服务器示例</span></div>
<pre class="crayon-plain-tag">#include &lt;sys/socket.h&gt;
#include &lt;netinet/in.h&gt;
#include &lt;arpa/inet.h&gt;
#include 
#include 
#include 
#include 
#include 
#include &lt;sys/types.h&gt;
#include  

int main(int argc, char *argv[])
{
    int listenfd = 0, connfd = 0;
    struct sockaddr_in serv_addr; 

    char sendBuff[1025];
    time_t ticks; 
    // 下面的调用在内核中创建一个未命名套接字，返回一个整数 —— 套接字描述符
    // AF_INET 地址族，对于IPv4使用AF_INET
    // SOCK_STREAM 传输层协议类型，流式表示需要确认机制
    // 0 让内核决定默认协议，AF_INET + SOCK_STREAM -&gt; TCP
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    memset(&amp;serv_addr, '0', sizeof(serv_addr));
    memset(sendBuff, '0', sizeof(sendBuff)); 
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_addr.sin_port = htons(5000); 
    // 将serv_addr所指定的细节信息，绑定到listenfd。bind调用时可选的，如果不调用，内核可以随机选取监听端口
    bind(listenfd, (struct sockaddr*)&amp;serv_addr, sizeof(serv_addr)); 

    // 在此套接字上监听新连接请求，最多10个排队
    listen(listenfd, 10); 

    while(1)
    {
        // accept导致当前线程休眠，当有新的客户端请求进入，并且完成3次握手后，线程醒来
        // 获得代表客户端套接字的套接字描述符
        connfd = accept(listenfd, (struct sockaddr*)NULL, NULL); 

        ticks = time(NULL);
        snprintf(sendBuff, sizeof(sendBuff), "%.24s\r\n", ctime(&amp;ticks));
        // 可以向套接字描述符中写数据
        write(connfd, sendBuff, strlen(sendBuff)); 
        // 关闭描述符
        close(connfd);
        sleep(1);
     }
}</pre>
<div class="blog_h3"><span class="graybg">TCP客户端示例</span></div>
<pre class="crayon-plain-tag">#include &lt;sys/socket.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;netinet/in.h&gt;
#include 
#include 
#include 
#include 
#include 
#include 
#include &lt;arpa/inet.h&gt; 

int main(int argc, char *argv[])
{
    int sockfd = 0, n = 0;
    char recvBuff[1024];
    struct sockaddr_in serv_addr; 

    if(argc != 2)
    {
        printf("\n Usage: %s  \n",argv[0]);
        return 1;
    } 

    memset(recvBuff, '0',sizeof(recvBuff));
    // 创建一个套接字，客户端服务器没有区别
    if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) &lt; 0)
    {
        printf("\n Error : Could not create socket \n");
        return 1;
    } 

    memset(&amp;serv_addr, '0', sizeof(serv_addr)); 

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(5000); 

    if(inet_pton(AF_INET, argv[1], &amp;serv_addr.sin_addr)&lt;=0)
    {
        printf("\n inet_pton error occured\n");
        return 1;
    } 

    // 客户端套接字一般都不需要绑定，由内核自由分配一个端口就足够了

    // 尝试连接到远程套接字（IP+端口）
    if( connect(sockfd, (struct sockaddr *)&amp;serv_addr, sizeof(serv_addr)) &lt; 0)
    {
       printf("\n Error : Connect Failed \n");
       return 1;
    } 
    // 读取套接字，就像读取一个普通文件一样
    while ( (n = read(sockfd, recvBuff, sizeof(recvBuff)-1)) &gt; 0)
    {
        recvBuff[n] = 0;
        if(fputs(recvBuff, stdout) == EOF)
        {
            printf("\n Error : Fputs error\n");
        }
    } 

    if(n &lt; 0)
    {
        printf("\n Read error \n");
    } 

    return 0;
}</pre>
<div class="blog_h1"><span class="graybg">轮询机制对比</span></div>
<div class="blog_h2"><span class="graybg">read</span></div>
<p>应用程序周期性的调用read来检查I/O状态，以完成数据的读取，性能最差。</p>
<div class="blog_h2"><span class="graybg">select</span></div>
<p>维护一个最长1024的数组，保存文件描述符的状态，一次select可以遍历很多文件描述符。</p>
<pre class="crayon-plain-tag">int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);</pre>
<p>select 函数监视的文件描述符分 3 类，分别是 writefds、readfds 和 exceptfds。<span style="background-color: #c0c0c0;">调用后 select 函数会阻塞，直到有描述符就绪</span>（有数据 可读、可写、或者有 except），<span style="background-color: #c0c0c0;">或者超时</span>（timeout 指定等待时间，如果立即返回设为 null 即可）。当 <span style="background-color: #c0c0c0;">select 函数返回后，通过遍历 fd_set，来找到就绪的描述符</span>。<br />select 目前<span style="background-color: #c0c0c0;">几乎在所有的平台上支持</span>，其良好跨平台支持是它的一大优点。select 的一个<span style="background-color: #c0c0c0;">缺点在于单个进程（Apache使用多进程方式解决此问题，尽管Linux进程很轻量，也是有代价的。而且进程间数据同步远比不上线程间同步的高效）能够监视的文件描述符的数量存在最大限制，在 Linux 上一般为 1024</span>，可以通过修改宏定义甚至重新编译内核的方式提升这一限制，但是这样也会造成效率的降低。</p>
<div class="blog_h2"><span class="graybg">poll</span></div>
<p>类似于select，但是使用链表解决1024数组长度限制。</p>
<pre class="crayon-plain-tag">int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};</pre>
<p>pollfd 结构包含了要监视的 event 和发生的 event，不再使用 select参数-值传递的方式。同时，<span style="background-color: #c0c0c0;">pollfd 并没有最大数量限制（但是数量过大后性能也是会下降）</span>。和 select 函数一样，<span style="background-color: #c0c0c0;">poll 返回后，需要轮询 pollfd 来获取就绪的描述符</span>。<br />从上面看，select 和 poll 都需要在返回后，通过遍历文件描述符来获取已经就绪的 socket。事实上，同时连接的<span style="background-color: #c0c0c0;">大量客户端在同一时刻可能只有很少的处于就绪状态，因此随着监视的描述符数量的增长，其效率也会线性下降</span>。</p>
<div class="blog_h2"><span class="graybg">epoll</span></div>
<p>效率最高，调用epoll后，如果没有发现可用的I/O事件，调用线程将会自动休眠，直到有事件发生后，内核将其唤醒。</p>
<pre class="crayon-plain-tag">// 创建 epoll 文件描述符，参数 size 并不是限制了 epoll 所能监听的描述符最大个数，只是对内核初始分配内部数据结构的一个建议
int epoll_create(int size)；
// 对指定描述符 fd 执行 op 操作控制，event 是与 fd 关联的监听事件
// op有三种：添加 EPOLL_CTL_ADD，删除 EPOLL_CTL_DEL，修改 EPOLL_CTL_MOD
//          分别添加、删除和修改对 fd 的监听事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)；

typedef union epoll_data {
    void *ptr;
    int fd;
    __uint32_t u32;
    __uint64_t u64;
} epoll_data_t;

struct epoll_event {
    __uint32_t events;      /* Epoll events */
    epoll_data_t data;      /* User data variable */
};
//  等待 epfd 上的 IO 事件，最多返回 maxevents 个事件
int epoll_wait(int epfd, struct epoll_event * events,  int maxevents, int timeout);</pre>
<p>在 select/poll 中，进程只有在调用一定的方法后，<span style="background-color: #c0c0c0;">内核才对所有监视的文件描述符进行扫描</span>，而 epoll 事先通过 epoll_ctl() 来注册一个文件描述符，<span style="background-color: #c0c0c0;">一旦某个文件描述符就绪时，内核会采用类似 callback 的回调机制，迅速激活这个文件描述符</span>，当进程调用 epoll_wait 时便得到通知</p>
<p>epoll 的优点主要是一下几个方面：</p>
<ol>
<li>监视的描述符数量不受限制，它所支持的 fd 上限是最大可以打开文件的数目，这个数字一般远大于 2048, 举个例子, 在 1GB 内存的机器上大约是 10 万左右，具体数目可以 <pre class="crayon-plain-tag">cat /proc/sys/fs/file-max</pre> 察看, 一般来说这个数目和系统内存关系很大</li>
<li>IO 的效率不会随着监视 fd 的数量的增长而下降。epoll 不同于 select 和 poll 轮询的方式，而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数</li>
<li>支持水平触发和边沿触发两种模式：
<ol>
<li>水平触发模式，文件描述符状态发生变化后，如果没有采取行动，它将后面反复通知，这种情况下编程相对简单，libevent 等开源库很多都是使用的这种模式</li>
<li>边沿触发模式，只告诉进程哪些文件描述符刚刚变为就绪状态，只说一遍，如果没有采取行动，那么它将不会再次告知。理论上边缘触发的性能要更高一些，但是代码实现相当复杂（Nginx 使用的边缘触发）</li>
</ol>
</li>
<li>mmap 加速内核与用户空间的信息传递。epoll 是通过<span style="background-color: #c0c0c0;">内核与用户空间 mmap 同一块内存，避免了无谓的内存拷贝</span></li>
</ol>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/linux-network-programming">Linux网络编程</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/linux-network-programming/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux网络知识集锦</title>
		<link>https://blog.gmem.cc/network-faq</link>
		<comments>https://blog.gmem.cc/network-faq#comments</comments>
		<pubDate>Fri, 22 Sep 2006 12:21:20 +0000</pubDate>
		<dc:creator><![CDATA[Alex]]></dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Network]]></category>
		<category><![CDATA[Linux知识]]></category>
		<category><![CDATA[Linux编程]]></category>
		<category><![CDATA[网络编程]]></category>

		<guid isPermaLink="false">http://blog.gmem.cc/?p=6337</guid>
		<description><![CDATA[<p>网络编程 参考： Linux网络编程 Linux编程知识集锦 Linux知识集锦 Bonding 网络接口绑定（Network Interface Bonding）是Linux下的一项技术，它能够将多块物理网卡绑定为单一的逻辑网卡，从而实现： 带宽增加 提供容错能力，防止一根网线损坏的情况 也叫Teaming、 Link Aggregation Groups（LAG）。 启用 你需要先安装bonding内核模块，并且用modprobe查看bonding驱动是否被加载： [crayon-69e20248bc740464139949/] 创建 你需要先安装两块物理NIC，然后使用下面的命令将它们bond为新的逻辑接口：  [crayon-69e20248bc745186260166/] 或者永久化的配置： <a class="read-more" href="https://blog.gmem.cc/network-faq">[...]</a></p>
<p>The post <a rel="nofollow" href="https://blog.gmem.cc/network-faq">Linux网络知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></description>
				<content:encoded><![CDATA[<div class="wri_content_clear_both"><div class="blog_h1"><span class="graybg">网络编程</span></div>
<p>参考：</p>
<ol>
<li><a href="/linux-network-programming">Linux网络编程</a></li>
<li><a href="/linux-programming-faq">Linux编程知识集锦</a></li>
<li><a href="/linux-faq">Linux知识集锦</a></li>
</ol>
<div class="blog_h1"><span class="graybg">Bonding</span></div>
<p>网络接口绑定（Network Interface Bonding）是Linux下的一项技术，它能够将多块物理网卡绑定为单一的逻辑网卡，从而实现：</p>
<ol>
<li>带宽增加</li>
<li>提供容错能力，防止一根网线损坏的情况</li>
</ol>
<p>也叫Teaming、 Link Aggregation Groups（LAG）。</p>
<div class="blog_h2"><span class="graybg">启用</span></div>
<p>你需要先安装bonding内核模块，并且用modprobe查看bonding驱动是否被加载：</p>
<pre class="crayon-plain-tag">sudo modprobe bonding
lsmod | grep bond</pre>
<div class="blog_h2"><span class="graybg">创建</span></div>
<p>你需要先安装两块物理NIC，然后使用下面的命令将它们bond为新的逻辑接口： </p>
<pre class="crayon-plain-tag">sudo ip link add bond0 type bond mode 802.3ad
sudo ip link set eth0 master bond0
sudo ip link set eth1 master bond0</pre>
<p>或者永久化的配置：</p>
<pre class="crayon-plain-tag">auto bond0
iface bond0 inet static
	address 192.168.1.150
	netmask 255.255.255.0	
	gateway 192.168.1.1
	dns-nameservers 192.168.1.1 8.8.8.8
	dns-search domain.local
		slaves eth0 eth1
		bond_mode 0
		bond-miimon 100
		bond_downdelay 200
		bound_updelay 200</pre>
<div class="blog_h1"><span class="graybg">TCP协议</span></div>
<p>参考：<a href="/tcp-ip-study-note">TCP/IP协议栈学习笔记</a></p>
<div class="blog_h2"><span class="graybg"><a id="connstate"></a>连接状态</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 20%; text-align: center;">状态</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">LISTEN</td>
<td>仅仅对于服务器端存在，正在指定的端口上监听</td>
</tr>
<tr>
<td class="blog_h3">ESTABLISHED</td>
<td>三次握手完成，连接建立</td>
</tr>
<tr>
<td class="blog_h3">FIN_WAIT1</td>
<td> </td>
</tr>
<tr>
<td class="blog_h3">CLOSE_WAIT</td>
<td> </td>
</tr>
<tr>
<td class="blog_h3">FIN_WAIT2</td>
<td>
<p>在FIN_WAIT_2状态，本端已经发送FIN，对端已经ACK。除非进行半关闭，否则对端的应用层已经意识到需要进行关闭，并向本端发送FIN来关闭另一方向的连接，只有对端完成这个关闭，本端才从FIN_WAIT_2进入TIME_WAIT状态。</p>
<p>这也意味着，本端可能一直处于FIN_WAIT_2，对端则一直处于CLOSE_WAIT，为了防止处于FIN_WAIT_2的无限等待，TCP实现中使用定时器进行处理</p>
</td>
</tr>
<tr>
<td class="blog_h3">TIME_WAIT</td>
<td>
<p>TIME_WAIT又称2MSL等待状态。</p>
<p>每个TCP实现必须选择一个报文段最大生存时间（Maximum Segment Lifetime，MSL）—— 是任何报文段被丢弃前在网络内的最长时间。此外，由于IP数据报具有TTL，因此在网络上存在是有限制的。后者是基于跳数，而不是定时器。MSL的值通常是 30秒、1分钟或者2分钟。</p>
<p>对一个具体实现所给定的MSL值，处理的原则是：当TCP执行一个主动关闭，并发回最后一个ACK，该连接必须在TIME_WAIT状态停留的时间为2倍的MSL，这样可让TCP再次发送最后的ACK以防这个ACK丢失（另一端超时并重发最后的FIN）。</p>
<p>2MSL等待的结果是，2MSL期间定义连接的Socket（即：唯一标识Socket的四元组）不能再被使用。但是某些实现允许重用处于2MSL状态的端口（指定：SO_REUSEADDR）</p>
<p>服务器通常是被动关闭，不会进入TIME_WAIT状态</p>
</td>
</tr>
<tr>
<td class="blog_h3">CLOSED</td>
<td>不是一个真实的状态，作为状态图假想的起终点</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">网络地址转换</span></div>
<p>网络地址转换也称为网络掩蔽或者<span style="background-color: #c0c0c0;">IP掩蔽（Masquerading）</span>，是一种在IP封包通过路由器或防火墙时<span style="background-color: #c0c0c0;">重写源IP地址或目的IP地址</span>的技术。该技术普遍用于只有一个公有IP的局域网中，允许局域网中多台主机与公网上的主机进行通信。NAT 功能通常被集成到路由器、防火墙、ISDN路由器或者单独的NAT设备中。</p>
<div class="blog_h2"><span class="graybg">NAT工作机制</span></div>
<p>一个典型的局域网会使用一个专用网络来指定子网，最常用的是192.168.x.x，这个局域网中包含一个路由器，它占用一个IP地址（例如192.168.0.1）。路由器同时通过一个ISP提供的公有IP地址连接到因特网上。</p>
<p>当局域网上的主机需要和公网主机通信时，其发送IP封包，路由器将IP封包的源地址从专有地址192.168.0.x转换为公有地址（例如106.185.46.7），路由记住专有地址[:端口]与公有地址[:端口]的映射关系，当公网主机的IP封包到达时，利用此映射关系改写目的地址，进而转发给局域网主机。</p>
<p>上述的源地址、目的地址改写的过程即为NAT。</p>
<div class="blog_h2"><span class="graybg">NAT的优缺点</span></div>
<p>NAT的优势：</p>
<ol>
<li>在IPv4短缺的情况下，可以使多台主机共享一个IP地址接入因特网</li>
<li>隐藏内网计算机，避免受到来自外部网络的攻击</li>
</ol>
<p>NAT的缺点：</p>
<ol>
<li>某些协议无法正常工作，例如主动模式的FTP。这些协议需要引入<span style="background-color: #c0c0c0;">应用层网关（Application Layer Gateway，ALG）</span>才能正常工作</li>
</ol>
<div class="blog_h2"><span class="graybg">NAT的分类</span></div>
<div class="blog_h3"><span class="graybg">基本网络地址转换</span></div>
<p>亦可简称NAT或者静态NAT，<span style="background-color: #c0c0c0;">仅支持地址转换，不支持端口映射</span>。需要每一个连接对应一个公网IP地址，因此需要维护一个公网IP地址池。某些宽带路由器使用这种方式去指定一台局域网主机去接受所有外部连接，并称该机器为<span style="background-color: #c0c0c0;">DMZ主机</span>（并不是真正意义上的，因为DMZ主机必须与内网隔离）。</p>
<div class="blog_h3"><span class="graybg">网络地址端口转换</span></div>
<p>NAPT，该方式<span style="background-color: #c0c0c0;">支持端口映射</span>，允许多台内网主机共享一个公网IP地址，其包括两类转换功能：</p>
<ol>
<li>源地址转换：发起连接的内网计算机的IP地址将会被重写，使得内网主机发出的数据包能够到达外网主机</li>
<li>目的地址转换：被连接内网计算机的IP地址将被重写，使得外网主机发出的数据包能够到达内网主机</li>
</ol>
<p>这两种转换一般会一起使用，以支持双向通信。</p>
<p>NAPT需要维护一个NAT表，来基于内网IP/端口与公网IP/端口的对应关系，例如：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="text-align: center;"> 内网IP/端口</td>
<td style="text-align: center;"> 外网IP/端口</td>
</tr>
</thead>
<tbody>
<tr>
<td>192.168.0.89:6443</td>
<td>106.185.46.7:9200</td>
</tr>
<tr>
<td>192.168.0.90:8897</td>
<td>106.185.46.7:9201 </td>
</tr>
</tbody>
</table>
<p> <a href="/webrtc-server-basedon-kurento#glossary-stun">STUN</a>标准将NAPT分为以下几个子类型：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类型 </td>
<td style="text-align: center;">说明 </td>
</tr>
</thead>
<tbody>
<tr>
<td>完全圆锥型NAT </td>
<td>一旦一个内部地址（iAddr:port1）映射到外部地址（eAddr:port2），<span style="background-color: #c0c0c0;">所有</span>发自iAddr:port1的包<span style="background-color: #c0c0c0;">都经由</span>eAddr:port2向外发送。<span style="background-color: #c0c0c0;">任意外部主机</span>（hAddr:*）都能通过给eAddr:port2发送封包到达iAddr:port1</td>
</tr>
<tr>
<td>地址受限圆锥型NAT</td>
<td>
<p>与完全圆锥形 NAT类似，但是外部主机（hAddr:*）能通过eAddr:port2给iAddr:port1发送封包的前提是，之前iAddr:port1<span style="background-color: #c0c0c0;">曾经发送</span>封包到hAddr:*。星号表示任意端口</p>
<p>在应用受限圆锥形NAT时，外网主机不能主动发起最初的通信，内网主机为了让外网主机能与之通信，主动发起连接的行为，被称为“打洞”</p>
</td>
</tr>
<tr>
<td>端口受限圆锥型NAT</td>
<td>与地址受限圆锥形NAT类似，但是附加了一个端口限制，即：外部主机（hAddr:port3）能通过eAddr:port2给iAddr:port1发送封包的前提是，之前iAddr:port1曾经发送封包到hAddr:port3</td>
</tr>
<tr>
<td>对称NAT（symmetric）</td>
<td>
<p>内部地址（iAddr:port1）向外部主机（hAddr:port3）发送封包，总是映射到同一个外部地址（eAddr:port2），不同的内部地址、外部主机地址组合总是映射到不同的外部地址。只有曾经收到过内部主机封包的外部主机，才能够把封包发回</p>
<p>一般注重安全性的大公司会启用对称NAT，以禁止P2P通信</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg"><a id="traversal"></a>NAT穿透</span></div>
<p>NAT-T技术主要解决两台同时处于NAT设备后面的局域网主机建立网络连接的问题。</p>
<p>两台都处于NAT后面的主机，无法知道对方映射的公网地址/端口，因此，一般的NAT-T技术都需要一个公共服务器作为媒介：要么在建立连接的时候需要用到该服务器，要么所有的数据都通过此公共服务器中继。</p>
<p>假设：</p>
<ol>
<li>位于NAT后的主机A（局域网IP：192.168.0.89，公网IP：106.185.46.7）</li>
<li>主机B（局域网IP：192.168.1.90，公网IP：106.185.46.10），以公网服务器S（106.185.46.1）为媒介（所谓<span style="color: #383838;">STUN服务器）</span>，进行NAT-T的过程可能如下（A与B是对称的）：</li>
<li>A连接S，获得映射地址（192.168.0.89:35330  =&gt;106.185.46.7:2250），将此地址信息告知S</li>
<li>B连接S，获得映射地址（192.168.1.90:35330 =&gt;106.185.46.10:3210），将此地址信息告知S</li>
<li>A和B分别获取对方的两个地址</li>
<li>A尝试连接B
<ol>
<li>如果B是完全圆锥型NAT，那么不论A是圆锥型、对称NAT，连接都立刻建立</li>
<li>如果B是地址或端口受限NAT，由于A之前从未与B通信过，因此无法建立连接
<ol>
<li>如果A是地址或端口受限NAT，与B的连接无法建立，但是A尝试发包到B的记录已经被A侧NAT记录。只要该记录没有过期，B就可以反过来向A发送数据，该数据 A必然收到，随后A和B即可双向通信</li>
<li>如果A是对称型NAT，B利用从S得到的关于A的信息（假设为192.168.0.89:35330 =&gt;106.185.46.7:2250 =&gt; 106.185.46.1:3387），尝试向106.185.46.7:2250发送信息，由于A的对称NAT的限制，此尝试必然失败。但是，此尝试在B侧留下了NAT记录。如果
<ol>
<li>B是地址受限NAT，那么A可以再次向B发送数据，这一次B必然收到</li>
<li>B是端口受限NAT，则无法通信</li>
</ol>
</li>
</ol>
</li>
<li><span style="background-color: #c0c0c0;">B是对称NAT的情况下，如果A是对称NAT或者端口受限圆锥型NAT</span>，则双方无法通信</li>
</ol>
</li>
</ol>
<div id="bridging" class="blog_h1"><span class="graybg">桥接</span></div>
<p><a id="bridging"></a>网桥（Bridge）是一种（可以是软件虚拟的）设备，该设备有<span style="background-color: #c0c0c0;">两个或者多个</span>网络接口，分别连接到多个局域网（不同网段）中。网桥将它连接的某个局域网发送的数据帧，统一转发到其它网络接口对应的局域网中，从而将多个网络在数据链路层无缝连接起来，就<span style="background-color: #c0c0c0;">好像是一个局域网</span>一样。任何真实设备（例如eth0）和虚拟设备（例如tap0）都可链接到网桥。</p>
<p>网桥与路由器不同，后者允许多个网络在保持独立的情况下能够相互通信。网桥的行为更像是一台虚拟的交换机，它能够透明的工作，其它主机不需要知道其存在。</p>
<p>Vmware中的桥接，和一般意义上的桥接原理是一样的，它让虚拟机网卡连接到一个虚拟的以太网交换机，虚拟以太网交换机与宿主机器的以太网卡通过一个“虚拟网桥”相连。</p>
<div class="blog_h2"><span class="graybg">Linux中的桥接</span></div>
<p>当你将一个网络接口（例如eth0）添加到Linux虚拟网桥（例如br0）时，你需要把IP地址从eth0移除，并添加到br0，这样才能保证网络正常工作。</p>
<p>这是因为，网桥br0负责处理本机eth0的入站流量，它应该响应ARP请求——这样它<span style="background-color: #c0c0c0;">才能把流量转发给网桥中的其它网络接口</span>，因而它拥有eth0的IP就很合理了（把网桥的MAC和IP关联）。</p>
<div class="blog_h3"><span class="graybg">Hairpin</span></div>
<p>默认情况下，Bridge<span style="background-color: #c0c0c0;">不允许包从收到包的端口发出</span>。例如，当Bridge从一个端口接收到广播报文后，它会将报文<span style="background-color: #c0c0c0;">向其它端口全部发出</span>，而不包括接收的那个端口。</p>
<p>你可以<span style="background-color: #c0c0c0;">在端口级别打开Hairpin模式</span>，这样，<span style="background-color: #c0c0c0;">从该端口进入网桥的包，还可以从此端口出去</span>。在NAT场景下，例如Docker的NAT网络，从容器访问它映射（80）到的主机端口（8080）时：</p>
<ol>
<li>容器访问hostip:8080</li>
<li>请求到达网桥，来自端口A</li>
<li>进入宿主机协议栈</li>
<li>DNAT转换为continerip:80</li>
<li>协议包又要从A端口发出</li>
</ol>
<p>为什么叫Hairpin，这是形象的描述流量的走向，就像最简单的U形发卡一样，封包经过网桥/路由处理后，从它来自的地方发回去。</p>
<div class="blog_h1"><span class="graybg">以太网监听</span></div>
<p>当一张网卡被设置为混杂（Promicuous）模式，它就会接收网络上所有的数据帧，从而可以实现监听。</p>
<div class="blog_h2"><span class="graybg">混杂模式网卡检测</span></div>
<p>有多种方式可以检测处于混杂模式的网卡，假设现在怀疑硬件地址为38-83-45-09-0A-60、IP地址为192.168.1.5的网卡处于混杂模式，可以：</p>
<ol>
<li>伪造ICMP报文：ECHO_REQUEST，即ping命令使用的报文，将其IP首部的目的地址设置为192.168.1.5，以太网帧首部的目的地址设置为虚假的地址00-00-00-00-00-00。如果192.168.1.5是正常的网卡，它将会检测硬件地址，发现与自己的地址不同，因而忽略此帧；反之，如果192.168.1.5处于混杂模式，则它不会做硬件地址的比较，报文直接交由上层处理，导致回复ICMP报文</li>
<li>以非广播方式在局域网内发送ARP请求，如果某个网卡回复之，则其可能处于混杂模式</li>
</ol>
<div class="blog_h2"><span class="graybg">监听防范</span></div>
<p>首先要保证局域网的安全，因此这种监听只能出现在局域网内，因此必须有以太局域网主机被攻破。</p>
<p>另外数据加密也是比较好的手段，如果监听到的报文是加密的，没有什么用。</p>
<p>使用交换机也是一种常见的方式，交换机工作在数据链路层，与工作在物理层的HUB不同。交换机通常会维护一个ARP数据库，记录每个交换机端口绑定的MAC地址，当报文到达交换机时，它<span style="background-color: #c0c0c0;">只会将其从匹配的端口发送出去</span>。交换机只在两种情况下广播报文：</p>
<ol>
<li>以太网帧的目的地址在本地ARP数据库中不存在</li>
<li>报文本身就是广播的</li>
</ol>
<p>交换机可以在很大程度上解决监听问题，但是不能防止ARP欺骗。</p>
<div class="blog_h1"><span class="graybg">隧道</span></div>
<p>所谓隧道（Tunneling），是指<span style="background-color: #c0c0c0;">使用一种网络协议，并将另外一个网络协议封装在其载荷（或称负载，Payload）部分</span>的技术。前者叫做隧道协议，后者叫做负载协议。使用隧道技术的目的一般是：</p>
<ol>
<li>在不兼容的网络上传送网络协议数据报</li>
<li>在不安全的网络上提供安全性保证</li>
<li>规避防火墙，被防火墙阻挡的协议可以封装在不被阻挡的协议中，例如HTTP</li>
</ol>
<p>通常，隧道协议往往位于负载协议的高层（例如PPTP，即点对点隧道协议，可以通过TCP来封装PPP，前者工作在传输层，而PPP是链路层协议），或者与负载协议处于同一层。</p>
<p>Linux L3的隧道技术，主要基于TUN设备实现。</p>
<p>常见的隧道协议包括：</p>
<table style="width: 100%;" border="1" cellspacing="0" cellpadding="5">
<thead>
<tr>
<td style="width: 100px; text-align: center;">协议 </td>
<td style="text-align: center;"> 说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>IPsec</td>
<td>即互联网安全协定（Internet Protocol Security）。该协议通过对IP协议的分组进行加密、认证，来保护TCP/IP协议族的安全性</td>
</tr>
<tr>
<td>GRE</td>
<td>即通用路由封装（Generic Routing Encapsulation）。可以在虚拟的点对点链路中封装多种网络层协议</td>
</tr>
<tr>
<td>IP in IP</td>
<td>一种IP隧道协定，可以将IP封包封装进另外一个IP封包中</td>
</tr>
<tr>
<td>L2TP</td>
<td>
<p>即第二层隧道协议（Layer Two Tunneling Protocol）。是一种VPN的实现方式。L2TP本身不提供加密和验证功能，需要和某种安全协议搭配使用（例如IPsec）</p>
<p>L2TP封包使用UDP来传送。每个高层协议，例如PPP，都可以在L2TP隧道中建立一个L2TP会话，一个隧道里面可以包含多个会话</p>
</td>
</tr>
<tr>
<td>PPTP</td>
<td>即点对点隧道协议（Point to Point Tunneling Protocol）。是一种VPN的实现方式。PPTP使用TCP来创建控制通道，来发送控制命令，并使用GRE通道来封装PPP协议数据报，以发送数据。该协议的加密方式容易被破解</td>
</tr>
<tr>
<td>PPPoE</td>
<td>即基于以太网的点对点隧道协议（Point-to-Point Protocol over Ethernet）。是将PPP协议封装在以太网中的一种隧道协议</td>
</tr>
<tr>
<td>PPPoA</td>
<td>即基于异步传输模式的点对点隧道协议（Point-to-Point Protocol over ATM）</td>
</tr>
<tr>
<td>SSH</td>
<td>即安全外壳协议（Secure Shell Protocol）。是一项跨越多个层次的协议，为Shell提供安全的传输和使用环境。SSH提供了数据压缩的功能。</td>
</tr>
<tr>
<td>SOCKS</td>
<td>即套接字安全协议（Socket Secure）。用于通过一个代理服务器在客户端和服务器之间路由数据包</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">网关</span></div>
<p>在传统TCP/IP术语中，网络设备只分为网关、主机（亦称终端系统）两种，<span style="background-color: #c0c0c0;">前者能够在网络间传递数据报</span>。网关的数据处理一般只到达第三层（IP层）。网关与路由器在传统TCP/IP术语中往往是一个概念。</p>
<p>现代网络术语中，网关与路由器不同，<span style="background-color: #c0c0c0;">网关能够在不同协议之间移动数据</span>；而路由器则是在不同网络之间移动数据。例如，语音网关可以连接公共交换电话网（PSTN）与以太网，实现网络电话。</p>
<div class="blog_h1"><span class="graybg">多播</span></div>
<p>多播（MultiCast）也称为组播，包括链路层、网络层、应用层等多播技术。多播通常指IP多播（网络层）。</p>
<p>由于在IPv4网络中，<span style="background-color: #c0c0c0;">多播包可能不被路由</span>，因此你只能在局域网中使用多播技术。DVMRP、MOSPF 之类的技术解决此问题，但是你需要能够对客户端 - 服务器之间所有路由器进行配置，或者创建隧道。IPv6网络是强制支持IP多播的。</p>
<div class="blog_h1"><span class="graybg">网络虚拟化</span></div>
<p>网络虚拟化是一个很大的技术方向，可以包括硬件、软件或者相结合的实现方式。</p>
<p>Linux内核支持的网络虚拟化，主要包括两个方面：</p>
<ol>
<li>软交换机：Linux Bridge, OpenVSwitch</li>
<li>虚拟网络适配器：tun、tap、veth等</li>
</ol>
<div class="blog_h2"><span class="graybg">VLAN</span></div>
<p>VLAN（Virtual Local Area Network）是广泛应用的<span style="background-color: #c0c0c0;">网络虚拟化（在一套物理网络设备上虚拟出多个二层网络）</span>技术，它直接在Ethernet帧的头部加上<span style="background-color: #c0c0c0;">4个字节的VLAN Tag</span>，此Tag用于标识不同的二层网络。VLAN已经在大部分的网络设备和操作系统中得到了支持，它处理起来也比较简单，在读取Ethernet数据的时候，只需要根据EtherType相应的偏移4个字节就行。利用VLAN可以：</p>
<ol>
<li>将主机进行分组，即使主机不是连接到同一交换机上</li>
<li>可以对连接到相同交换机/网桥上的主机/客户机进行隔离，划分到不同子网。这样一个交换机就可以表现的像是多个独立交换机一样</li>
</ol>
<p>VLAN ID的范围是1 - 4095，对于规模较大的IT组织，需要谨慎规划子网，否则VLAN ID可能不够用。</p>
<div class="blog_h3"><span class="graybg">理解VLAN</span></div>
<p>假设你的IT环境下有一个物理交换机，连接着一系列物理主机，现在需要划分为三个独立网络。管理员配置交换机端口，将它们的VLAN ID分别设置为10 11 12，不同网络的主机分别连接到对应的端口。这种仅仅配置针对单个VLAND的端口，叫做access port。</p>
<p>随着规模的增大，一个交换机不够用了，管理员又引入一个交换机。这样，两个交换机必须都有一个端口，它允许任何以太网帧通过，而不管帧的VLAN ID，否则就无法联通了。这种不过滤VLAN ID的端口叫做trunk port。</p>
<p>在<span style="background-color: #c0c0c0;">虚拟化环境下，交换机的所有端口都会配置为trunk port。这样，每个端口（也就是主机）上都可以运行不同虚拟网络的VM</span>。</p>
<div class="blog_h2"><span class="graybg">VXLAN</span></div>
<p>VXLAN（Virtual eXtensible Local Area Network）是目前最热门的网络虚拟化/Overlay/虚拟隧道技术之一，VXLAN协议将<span style="background-color: #c0c0c0;">Ethernet帧封装在底层网络（Underlay）的三层报文（UDP）内，再加上8个字节的VXLAN header</span>，用来标识不同的二层网络：</p>
<p><a href="/wp-content/uploads/2019/04/106_net_005_vxlan.png"><img style="width: 100%;" src="/wp-content/uploads/2019/04/106_net_005_vxlan.png" alt="" /></a></p>
<p><span style="color: #1a1a1a;">VXLAN数据是经过<span style="background-color: #c0c0c0;">VTEP（VXLAN Tunnel EndPoint）封装和解封装</span>的，相应的VXLAN数据的外层UDP封包的IP（源、目的）地址就是VTEP（本机、对方）的IP地址，端口就是VTEP设备的端口（默认4789）。最外层的MAC地址用来实现VTEP之间的数据传递。每个物理节点上的所有虚拟机可以共享VTEP。VTEP可以由软件（例如OVS）或硬件实现。</span></p>
<p>VXLAN因为提出的较晚，在设备上的支持率不如VLAN，而且，VXLAN数据的封装解封装，要比VLAN复杂的多。但是它具有以下优势：</p>
<ol>
<li>VLAN ID数量限制：8个字节的VXLAN Header。其中的24bit用来标识（VNI，VXLAN Network Identifier）不同的二层网络，这样总共可以<span style="background-color: #c0c0c0;">标识1600多万个不同的二层网络</span></li>
<li>TOR交换机MAC地址表限制：在网络虚拟化之前，TOR交换机的一个端口连接一个物理主机对应一个MAC地址，但现在交换机的一个端口虽然还是连接一个物理主机但是可能进而连接<span style="background-color: #c0c0c0;">几十个甚至上百个虚拟机和相应数量的MAC地址</span>，MAC地址表记录在交换机的内存中，而交换机的内存是有限的。如果使用VXLAN，虚拟机的以太网帧被VTEP封装在UDP里面，<span style="background-color: #c0c0c0;">一个VTEP可以被一个物理主机上的所有虚拟机共用</span></li>
<li>灵活的虚机部署：采用VLAN网络的虚拟环境，不存在Overlay网络。虚拟机的网络数据，被打上VLAN Tag之后，直接在物理网络上传输，与物理网络上的VLAN是融合在一起的。这种实现机制的好处是：虚拟机能直接访问到物理网络的设备。而坏处是，无法突破物理网络的限制，通常<span style="background-color: #c0c0c0;">不同的VLAN网络，会被分配不同的IP地址段</span>，通过路由器或者其他的三层设备连接在一起，不同VLAN的虚拟机不能方便的进行L2通信。使用VXLAN后，由于基于UDP进行封装，因此可以在L2或L3网络上构建L2网络，这是一个独立于物理网络的Overlay network</li>
</ol>
<p>和普通隧道（例如ipip）不同，VXLAN是一对多的，而不是1：1的隧道协议。VXLAN设备可以像网桥那样，动态添加对端IP地址。严格来说，VXLAN模型中并没有隧道的物理实体。</p>
<p>配置和管理VXLAN，需要内核版本3.7以上的iproute2包，ip命令即此包的用户空间工具。</p>
<div class="blog_h3"><span class="graybg">工作原理</span></div>
<p><span style="color: #1a1a1a;">VXLAN报文的转发过程如下：</span></p>
<ol>
<li>原始报文经过源主机上的VTEP设备，被Linux内核添加VXLAN包头、外层UDP头，发送出去</li>
<li>对端VTEP设备接收到VXLAN报文，移除UDP头，然后根据VXLAN头决定发给哪个虚拟机</li>
</ol>
<p>在通信之前，需要回答以下三个问题：</p>
<ol>
<li>哪些VTEP需要加到同一VNI组</li>
<li>源虚拟机如何知道目的虚拟机的MAC地址</li>
<li>如何知道目的虚拟机在哪个节点</li>
</ol>
<p>问题1，通常由管理员进行配置。问题2/3本质上是一个问题 —— VXLAN通信双方如何感知彼此：</p>
<ol>
<li>内层报文，双方IP地址可以认为是已知</li>
<li>内层报文，对方MAC地址，需要实现一种ARP机制</li>
<li>VXLAN头，只需要知道<span style="background-color: #c0c0c0;">VNI，通常直接配置在VTEP上</span> —— 要么提前规划，要么根据内层报文自动生成</li>
<li>UDP头，需要知道源、目的地址/端口。源地址端口自动获取，目的端口一般默认4789，目的IP地址亦即目标主机的VTEP地址，可以通过两种方式得到：
<ol>
<li>组播：同一个VXLAN网络的VTEP加入到同一组播网络中，通过组播同步信息</li>
<li>控制中心：集中式保存需要的信息</li>
</ol>
</li>
</ol>
<div class="blog_h3"><span class="graybg">点对点VXLAN</span></div>
<p>这种配置下，两台机器构成一个VXLAN网络，每个机器上配置一个VTEP，VTEP之间通过它们的IP地址进行通信。</p>
<p>首先，需要添加VXLAN类型的接口：</p>
<pre class="crayon-plain-tag"># 接口类型  VNI  对端端口
ip link add vxlan0 type vxlan id 1 dstport 4789 
  # 对端Underlay地址  本地Underlay地址   物理网络接口
  remote 192.168.1.3 local 192.168.1.2 dev ens33</pre>
<p>为VTEP分配IP地址并启用之：</p>
<pre class="crayon-plain-tag"># 自动添加路由 172.17.1.0      0.0.0.0         255.255.255.0   U     0      0        0 vxlan0
ip addr add 172.17.1.2/24 dev vxlan0
ip link set vxlan0 up</pre>
<p>此时你可以看到如下FDB（Forwarding information base，也叫MAC表）表项： </p>
<pre class="crayon-plain-tag">bridge fdb
00:00:00:00:00:00 dev vxlan0 dst 192.168.1.3 via ens33 self permanent</pre>
<p>该项的含义是， 默认的VTEP对端地址为192.168.1.3。原始报文经过vxlan0接口后，内核会为其添加VXLAN头部，外部封装的UDP包的目的IP地址会被设置为192.168.1.3。</p>
<p>在对端，你需要进行对应的配置，VNI必须保持一致。 </p>
<div class="blog_h3"><span class="graybg">多播VXLAN</span></div>
<p>该模式需要Underlay网络支持组播。首先创建VTEP：</p>
<pre class="crayon-plain-tag"># VNI                    # 使用ens33上的多播组239.1.1.1进行信息交换
ip link add vxlan0 type vxlan id 1  local 192.168.1.2  group 239.1.1.1 dev ens33 dstport 4789</pre>
<p>和点对点模式类似的，你需要为VTEP配置IP地址并且启用之。此时，你可以看到如下FDB表项：</p>
<pre class="crayon-plain-tag">00:00:00:00:00:00 dev vxlan0 dst 239.1.1.1 via ens33 self permanent</pre>
<p>和点对点模式不同的是，dst字段的值设置为组播地址，而非对端Underlay IP地址。这个条目会导致默认情况下UDP头的目的地址被设置为组播地址239.1.1.1。</p>
<p>在所有参与到VXLAN的节点上，你需要进行对应的配置，VNI必须保持一致。</p>
<p>该模式下VTEP之间通信过程如下：</p>
<ol>
<li>主机1通过vxlan0发起ping 172.17.1.3报文到主机2的vxlan0</li>
<li>主机1内核发现目的地址和源地址在同一二层网络中，需要获取对方MAC地址，但是本地没有缓存，因此发送ARP查询请求</li>
<li>ARP报文源MAC地址为主机1的vxlan0的MAC地址，目的地址为广播地址255.255.255.255</li>
<li>内核将ARP报文封装到UDP中，设置VXLAN头。由于VTEP配置了多播组，同时<span style="background-color: #c0c0c0;">不知道目标VTEP在哪台主机上，因此会从239.1.1.1发送组播</span>报文</li>
<li>多播组中所有主机都会收到UDP报文，并根据VXLAN头发送给对应的VTEP</li>
<li>VTEP去掉VXLAN头，得到ARP请求报文，并<span style="background-color: #c0c0c0;">将源VTEP的MAC地址+源主机IP地址信息记录到FDB表</span>中</li>
<li>主机2发现ARP请求是针对自己的，因此生成ARP应答，并且单播（因为已经知道主机1的IP-MAC对应关系）给主机1的VTEP</li>
<li>ARP应答通过底层网络发送给主机1，解析后发送给vxlan0，VTEP解析ARP报文并更新VTEP缓存，同时根据报<span style="background-color: #c0c0c0;">文学习得到目的VTEP所在的主机地址，添加到自己的FDB表</span></li>
</ol>
<div class="blog_h3"><span class="graybg">VXLAN+桥接</span></div>
<p>实际情况下，每个主机都可能有几十甚至上百个虚拟机/容器，需要加入到同一个VLAN中，而<span style="background-color: #c0c0c0;">每个VLAN在一台主机上仅仅有一个VTEP</span>。</p>
<p>一个简单的解决办法是桥接。对于容器场景，可以用VETH Pair将容器连接到网桥，然后将VTEP也连接到网桥。VTEP通过物理网络相互联系。</p>
<div class="blog_h3"><span class="graybg">分布式控制中心</span></div>
<p>由于某些网络设备不支持多播，而且多播导致的不必要流量，<span style="background-color: #c0c0c0;">在生产环境下VXLAN多播模式用的很少</span>。</p>
<p>生产环境主要使用分布式控制中心架构，<span style="background-color: #c0c0c0;">在每个VTEP节点部署Agent，Agent联系控制中心，获取通信所需要的信息（FDB+ARP）</span>。</p>
<div class="blog_h2"><span class="graybg">Linux Bridge</span></div>
<p>L2设备，一种虚拟的网桥（Bridge），它是：</p>
<ol>
<li>一种<span style="background-color: #c0c0c0;">网络设备，因此可以配置IP地址、MAC地址</span></li>
<li>虚拟交换机，具有和物理交换机类似的能力</li>
</ol>
<p>普通网络设备只有两个Port，一端的数据会从另外一端出去。例如物理网卡，会从外部接收数据，发往内核协议栈，或者从内核协议栈接收数据发送到外部。</p>
<p>Bridge则不同，它具有多个Port，数据可以从任何端口进来，至于从哪个端口出去，原理类似于物理交换机 —— 要看MAC地址。</p>
<p>Bridge被广泛用于KVM/QEMU、容器技术。</p>
<p>下面的命令创建并启用一个网桥：</p>
<pre class="crayon-plain-tag">ip link add name br0 type bridge
ip link set br0 up

# 或者使用brctl命令
brctl addbr br0</pre>
<p>刚创建的网桥，一端连接着网络协议栈，其余什么都没有连接，因此没有任何功能。</p>
<p>下面我们创建veth对：</p>
<pre class="crayon-plain-tag">ip link add veth0 type veth peer name veth1
ip addr add 1.2.3.101/24 dev veth0
ip addr add 1.2.3.102/24 dev veth1
ip link set veth0 up
ip link set veth1 up


# 接受源自本地IP的ARP包
echo 1 &gt; /proc/sys/net/ipv4/conf/all/accept_local 
# 不进行源地址校验
echo 0 &gt; /proc/sys/net/ipv4/conf/all/rp_filter 
echo 0 &gt; /proc/sys/net/ipv4/conf/veth0/rp_filter 
echo 0 &gt; /proc/sys/net/ipv4/conf/veth1/rp_filter


# 现在测试是可以通的
#         由于目的地址是本地地址，默认会走lo，因此用 -I 强制指定PING请求的源地址/源接口
ping -c 1 -I veth0 1.2.3.102</pre>
<p>并且将其一端连接到网桥： </p>
<pre class="crayon-plain-tag">ip link set dev veth0 master br0
# 或者使用brctl命令
brctl addif br0 veth0</pre>
<p>使用下面的命令查看网桥上连接了哪些设备：</p>
<pre class="crayon-plain-tag">bridge link
# 9: veth0 state UP : &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 master br0 state forwarding priority 32 cost 2 

# 或者使用brctl命令
brctl show
# bridge name	bridge id		   STP enabled	interfaces
# br0		    8000.4a4c11893317	no		     veth0
# docker0		8000.02428b446c5b	no		
# virbr0		8000.deadbeef0000	no		     virbr0-nic</pre>
<p>现在：</p>
<ol>
<li>br0和veth0连接起来，<span style="background-color: #c0c0c0;">双向通道</span></li>
<li>协议栈和veth0是单向通道，<span style="background-color: #c0c0c0;">协议栈可以发数据给veth0</span>，<span style="background-color: #c0c0c0;">veth0从外部接受的数据不能发送给协议栈</span></li>
<li>br0的MAC地址变成veth0的<span style="background-color: #c0c0c0;">MAC地址 </span></li>
</ol>
<p>相当于<span style="background-color: #c0c0c0;">br0在veth0和协议栈之间做了拦截</span> —— 本来veth0发给协议栈的数据，全部转发给br0处理了。 </p>
<p>而且，veth0和veth1之间也不通了：</p>
<pre class="crayon-plain-tag">ping -c 1 -I veth0 1.2.3.102
# From 1.2.3.101 icmp_seq=1 Destination Host Unreachable</pre>
<p>原因是什么呢？下面在veth1、veth0、br0上分别抓包：</p>
<pre class="crayon-plain-tag">tcpdump -n -i veth0
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on veth0, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291125 ARP, Request who-has 1.2.3.102 tell 1.2.3.101, length 28
# 22:37:16.291165 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28

tcpdump -n -i veth1
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on veth1, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291140 ARP, Request who-has 1.2.3.102 tell 1.2.3.101, length 28
# 22:37:16.291163 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28


tcpdump -n -i br0
# tcpdump: WARNING: br0: no IPv4 address assigned
# tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
# listening on br0, link-type EN10MB (Ethernet), capture size 65535 bytes
# 22:37:16.291165 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28
# 22:37:17.289325 ARP, Reply 1.2.3.102 is-at ce:97:9b:c2:d1:ee, length 28</pre>
<p>可以看到，veth0、veth1正常进行了ARP<span style="background-color: #ff9900;">请求</span>、<span style="background-color: #99cc00;">响应</span>，br0则仅仅监控到<span style="background-color: #cc99ff;">响应</span>。但是veth0却不能将ARP应答发给协议栈，因此无法通信。 具体分析：</p>
<ol>
<li>Ping命令发起ICMP请求，设置IP封包的源接口为veth0</li>
<li>内核接受到上述封包，发送到目的地址1.2.3.102之前，发现没有ARP缓存，无法生成L2帧</li>
<li>内核发送ARP请求，从veth0<span style="background-color: #ff9900;">发出</span></li>
<li>veth1接口接收到ARP请求，并正常<span style="background-color: #99cc00;">响应</span>之</li>
<li>veth0接收到<span style="background-color: #ff9900;">响应</span></li>
<li>veth0将响应<span style="background-color: #cc99ff;">通过交换机br0</span>发送，而非内核</li>
<li>内核没有接收到ARP响应，L2帧发送失败，Ping失败</li>
</ol>
<p>下面，把veth0的IP地址转让给br0：</p>
<pre class="crayon-plain-tag">ip addr del 1.2.3.101/24 dev veth0
ip addr add 1.2.3.101/24 dev br0</pre>
<p>现在，veth0没有IP地址了，因此协议栈在路由的时候，不会将数据包发送给veth0。这意味着，协议栈到veth0的单向通道也断了，<span style="background-color: #c0c0c0;">veth0单纯变成了连接br0和veth1的网线</span>。 </p>
<p>从br0 Ping veth1现在可以收到ICMP应答报文。报文流转路线：内核 - br0 - veth0 - veth1 - 内核 - veth1 - veth0 - br0 - 内核，但是却还不能联系到外部网络。另外，veth0仍然无法Ping通veth1，原因还是无法将ARP应答返回给内核。 </p>
<p>现在，再把物理网卡添加到网桥：</p>
<pre class="crayon-plain-tag">ip link set dev eth0 master br0</pre>
<p>现在eth0也和veth0一样，接受到的封包直接转发给br0，而不发给协议栈，自己变为一根网线。eth0、veth0无法Ping通网关，而br0则通过eth0这根网线连接到网关，veth1也可以通过br0 ping通网关。</p>
<p>这时，eth0已经成为一根网线，它上面配置IP没有意义，反而会影响路由表：</p>
<pre class="crayon-plain-tag">ip addr del 192.169.1.105/24 dev eth0</pre>
<p>eth0被删除后默认路由消失，重新添加默认路由后，可以从veth1连接外部网络。</p>
<div class="blog_h3"><span class="graybg">网桥和虚拟化</span></div>
<p>对于虚拟机来说，通常<span style="background-color: #c0c0c0;">通过tun/tap等网络设备，将虚拟机内的网卡连接到网桥</span>，再由网桥转发给出去。</p>
<p>对于容器来说，每个则通常使用veth对。容器的网关被桥接到br0，且网关设置为br0。容器IP封包发到br0后，进入宿主机协议栈。宿主机需要配置IP转发功能，可以把容器IP封包转发出去。由于容器、宿主机物理网络通常不是一个网段，因此转发出去之前通常需要NAT。</p>
<div class="blog_h3"><span class="graybg">网桥和混杂模式</span></div>
<p>混杂模式下，网卡会把所有接收到的流量交给协议栈处理，不管目的MAC地址是否匹配。</p>
<p>使用下面的命令，使网卡进入混杂模式：</p>
<pre class="crayon-plain-tag">ifconfig eth0 promisc</pre>
<p>使用下面的命令则退出混杂模式：</p>
<pre class="crayon-plain-tag">ifconfig eth0 -promisc</pre>
<p>需要注意的时，<span style="background-color: #c0c0c0;">加入到网桥后，设备自动进入混杂模式</span>。退出网桥后则自动退出混杂模式。另外加入网桥期间，设备无法退出混杂模式。  </p>
<div class="blog_h2"><span class="graybg">Bridge+VLAN</span></div>
<p>通过网桥，可以协助处理虚拟化/命名空间下的基于多VLAN的网络虚拟化。</p>
<div class="blog_h3"><span class="graybg">VLAN filtering</span></div>
<p>VLAN filtering是在3.8引入的特性，可以简化基于VLAN的网络虚拟化的配置复杂度。利用VLAN filtering，管理员不需要创建大量的VLAN、Bridge。<span style="background-color: #c0c0c0;">使用一个网桥，你就能够控制所有VLAN</span>。</p>
<p>这里我们看一个在虚拟化中广泛使用的网络拓扑：不同子网的虚拟机通过网桥，通过bond做负载均衡，最后连接到物理网卡。</p>
<p>没有VLAN filtering之前，拓扑图如下：</p>
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_original.png"><img class="size-full wp-image-32969 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_original.png" alt="bridge_original" width="538" height="370" /></a></p>
<p>创建此拓扑的步骤：</p>
<ol>
<li>创建bond设备，作为两张物理网卡的master：<br />
<pre class="crayon-plain-tag">ip link add bond0 type bond
ip link set bond0 type bond miimon 100 mode balance-alb

ip link set eth0 down
ip link set eth0 master bond0

ip link set eth1 down
ip link set eth1 master bond0

ip link set bond0 up </pre>
</li>
<li>在bond0上创建VLAN子接口：<br />
<pre class="crayon-plain-tag">ip link add link bond0 name bond0.2 type vlan id 2
ip link set bond0.2 up

ip link add link bond0 name bond0.3 type vlan id 3
ip link set bond0.3 up</pre>
</li>
<li>
<p>将VLAN子接口分别连接到一个网桥：</p>
<pre class="crayon-plain-tag">ip link add br0 type bridge
ip link set bond0.2 master br0
ip link set br0 up

ip link add br1 type bridge
ip link set bond0.3 master br1
ip link set br1 up </pre>
</li>
<li>
<p>将代表虚拟机的TAP设备也连接到网桥：
<pre class="crayon-plain-tag">ip link set guest_1_tap_0 master br0
ip link set guest_2_tap_0 master br0

ip link set guest_2_tap_1 master br1
ip link set guest_3_tap_0 master br1 </pre>
</li>
</ol>
<p>如果使用了VLAN filtering，就不需要创建bond的VLAN子接口，也仅需要一个网桥：
<p><a href="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_current.png"><img class="size-full wp-image-32971 aligncenter" src="https://cdn.gmem.cc/wp-content/uploads/2020/06/bridge_current.png" alt="bridge_current" width="541" height="374" /></a></p>
<p>创建此拓扑的步骤：</p>
<ol>
<li>
<p> 创建bond设备：</p>
<pre class="crayon-plain-tag">ip link add bond0 type bond
ip link set bond0 type bond miimon 100 mode balance-alb
ip link set eth0 down
ip link set eth0 master bond0
ip link set eth1 down
ip link set eth1 master bond0
ip link set bond0 up</pre>
</li>
<li>
<p>创建网桥，启用VLAN Filtering： 
<pre class="crayon-plain-tag">ip link add br0 type bridge
ip link set br0 up
ip link set br0 type bridge vlan_filtering 1

ip link set bond0 master br0</pre>
</li>
<li>
<p> 将TAP连接到网桥：
<pre class="crayon-plain-tag">ip link set guest_1_tap_0 master br0
ip link set guest_2_tap_0 master br0

ip link set guest_2_tap_1 master br0
ip link set guest_3_tap_0 master br0</pre>
</li>
<li>为这些TAP接口、Bond接口设置VLAN filter：<br />
<pre class="crayon-plain-tag">bridge vlan add dev guest_1_tap_0 vid 2 pvid untagged master
bridge vlan add dev guest_2_tap_0 vid 2 pvid untagged master

bridge vlan add dev guest_2_tap_1 vid 3 pvid untagged master
bridge vlan add dev guest_3_tap_0 vid 3 pvid untagged master

bridge vlan add dev bond0 vid 2 master
bridge vlan add dev bond0 vid 3 master </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">TUN/TAP</span></div>
<p>简单的理解：TUN/TAP就是针对用户空间程序而不是物理媒体的网络接口，这种网络接口的<span style="background-color: #c0c0c0;">一端连接着网络协议栈，另一端连接着用户空间程序</span>。不同于普通靠硬件板卡实现的设备，TUN/TAP全部用软件实现，并向运行于操作系统上的<span style="background-color: #c0c0c0;">软件提供与硬件的网络设备完全相同的功能</span>。
<p>TUN/TAP为用户空间程序提供数据包的收发功能，它可以呈现为简单的点对点设备或者以太网设备，与物理设备不同的是，它<span style="background-color: #c0c0c0;">从用户空间程序</span>接收数据包<span style="background-color: #c0c0c0;">而不是物理媒体</span>；同样的，它将数据包发送给用户空间程序，而不是物理媒体。操作系统通过TUN/TAP设备发送的数据包，被分发给关联到这些设备的用户空间程序中。用户空间程序也可以将数据包发给TUN/TAP设备，后者把数据包注入到操作系统的网络栈之中，模拟为来自外部的数据。</p>
<p>要使用TUN驱动，必须打开/dev/net/tun文件，并发起相应的ioctl()调用，来<span style="background-color: #c0c0c0;">向内核注册一个网络设备</span>，这些网络设备可以通过ifconfig看到，名字为tunXXX或者tapXXX（依据调用时的选项）。当关闭文件描述符时，这些网络设备连同相应的路由会消失。用户程序写入/dev/net/tun文件的数据，会写到内核网络协议栈；用户程序从/dev/net/tun文件读取时，则拿到内核发送给tun设备的IP封包。</p>
<p>要使用TAP驱动，基本和TUN完全相同，区别有几点：</p>
<ol>
<li>tun设备的/dev/tunX工作在L3，可以通过IP转发和物理网卡连通</li>
<li>tap设备的/dev/tapX工作在L2，可以和物理网卡进行桥接</li>
</ol>
<div class="blog_h3"><span class="graybg">TUN</span></div>
<p><span style="background-color: #c0c0c0;">TUN模拟了网络层（点对点）设备</span>，操作第三层数据包比如IP数据封包。普通的网卡通过网线收发数据包，但是 TUN 设备通过一个文件收发数据包。所有对这个文件的<span style="background-color: #c0c0c0;">写操作会通过 TUN 设备转换成一个数据包送给内核</span>；当内核发送一个包给 TUN 设备时，通过<span style="background-color: #c0c0c0;">读这个文件可以拿到数据包的内容</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/tun.png"><img class="aligncenter size-full wp-image-26513" src="https://blog.gmem.cc/wp-content/uploads/2010/05/tun.png" alt="tun" width="590" height="266" /></a></p>
<p>TUN 设备的 /dev/tunX 文件收发的是 IP 层数据包，只能工作在 IP 层，<span style="background-color: #c0c0c0;">无法与物理网卡做 bridge</span>，但是可以通过三层交换（如 ip_forward）与物理网卡连通。</p>
<p>TUN的含义即隧道，实际上TUN的确和隧道技术有关系。tun/tap设备将协议栈的部分数据转发给用户空间程序，使之有处理数据包的机会，例如加密、压缩。VPN是最常见的应用场景。下面是基于TUN设备实现VPN的过程：</p>
<ol>
<li>本地应用构造一个数据包，发送给tun0所在网段（虚拟网络）的IP地址192.168.1.3</li>
<li>数据包到达内核协议栈后，根据目的IP地址判断需要从tun0发出</li>
<li>tun0的一端连接着内核协议栈，另一端则连接着用户空间应用 —— VPN程序</li>
<li>VPN程序对数据包进行再次封装，源地址设置为eth0，目的地址设置为eth0所在网段的VPN对端物理机IP地址</li>
<li>封装后数据包发送到协议栈，走eth0发送到VPN对端</li>
</ol>
<p>Linux的L3隧道技术都是基于TUN设备，包括ipip、GRE、sit、ISATAP、VTI等。</p>
<div class="blog_h3"><span class="graybg">TAP </span></div>
<p>TAP 等同于一个以太网设备，它操作第二层数据包如以太网数据帧。TAP的工作方式和TUN完全相同。</p>
<p>TAP 设备的 /dev/tapX 文件收发的是 MAC 层数据包，拥有 MAC 层功能，<span style="background-color: #c0c0c0;">可以与物理网卡做 bridge</span>，支持 MAC 层广播。</p>
<p>Libvirt创建的VM，会在宿主机上对应一个vnet*接口，这个接口桥接到网桥设备virbr*：</p>
<pre class="crayon-plain-tag">virbr0		8000.100000000000	no		virbr0-nic
							vnet0
							vnet1</pre>
<p>这些vnet接口，就是TAP设备，它关联到一个进程，即运行qemu-kvm模拟器的那个进程。qemu-kvm写入到此接口的数据，在宿主机看来，就好像从vnet这个网卡接收到的封包似的。反之亦然，qemu-kvm从此接口读取的数据，会写入到虚拟机的eth0，在虚拟机看来，就好像是从网卡接收到封包似的。</p>
<div class="blog_h2"><span class="graybg">VETH</span></div>
<p>也是虚拟的以太网设备，可以作为<span style="background-color: #c0c0c0;">网络命名空间的隧道，让两个网络命名空间可以通信</span>。VETH也能够作为独立网络设备使用。从效果上说，VETH就像是两个网络接口通过RJ45网线连接在一起似的。</p>
<p>VETH设备总是成对形式创建的：</p>
<pre class="crayon-plain-tag"># 创建一对veth，相互连接起来
ip link add veth0 type veth peer name veth1

# 查看veth对
ip link list
# 13: veth1@veth0: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 72:a4:a7:eb:e8:db brd ff:ff:ff:ff:ff:ff
# 14: veth0@veth1: &lt;BROADCAST,MULTICAST,M-DOWN&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 46:ba:9b:5e:3a:27 brd ff:ff:ff:ff:ff:ff</pre>
<p><span style="background-color: #c0c0c0;">从一端发送的数据，会立即在另一端接收到</span>。任何一端Down掉或删除，则veth对的状态被破坏。</p>
<p>可以把veth放到其他网络命名空间：</p>
<pre class="crayon-plain-tag"># 创建一个新的网络命名空间
ip netns add test

# 移动veth1
ip link set veth1 netns test</pre>
<p>移动了以后，从当前命名空间看到的，Peer的名字会变化：</p>
<pre class="crayon-plain-tag"># 默认命名空间看veth1变为if13，表示Peer是test命名空间序号为13的网络接口
ip link show veth0
# 14: veth0@if13: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#     link/ether 46:ba:9b:5e:3a:27 brd ff:ff:ff:ff:ff:ff link-netnsid 3

# test命名空间看veth0变为if14，和上面的序号14匹配
ip netns exec test ip link show veth1
# 13: veth1@if14: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
#    link/ether 72:a4:a7:eb:e8:db brd ff:ff:ff:ff:ff:ff link-netnsid 0</pre>
<p>在宿主机上，通过<span style="background-color: #c0c0c0;">设置路由</span>，可以让其他网络命名空间的流量通过veth对出站。 </p>
<div class="blog_h3"><span class="graybg">和TAP区别</span></div>
<p>VETH和TAP都是用来传递L2以太网帧的，<span style="background-color: #c0c0c0;">TAP/TUN常用于用于加密、VPN、隧道、虚拟机</span>，VETH常用于不同命名空间之间进行数据穿越。这有历史问题在里面。</p>
<p>下面是OpenStack创建了新的虚拟机vm0后的网络架构图：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2006/09/Openstack-VM-details.png"><img class="aligncenter size-full wp-image-27119" src="https://blog.gmem.cc/wp-content/uploads/2006/09/Openstack-VM-details.png" alt="openstack-vm-details" width="700" height="471" /></a></p>
<p>关于虚拟机vm0的网络模型，说明如下：</p>
<ol>
<li>虚拟机中的eth0，连接到vnet0，vnet0是Linux网桥<span style="background-color: #c0c0c0;">qbrXXX的TAP接口</span></li>
<li>Linux网桥qbrXXX连接到OVS网桥br-int，通过一对VETH qvbXXX - qv0XXX</li>
</ol>
<p>可以看到，通过TAP将虚拟机连接到第一个网桥qbrXXX，而VETH对则将第一个网桥连接到第二个。为什么需要两根RJ45网线呢？用其中一个不行么？</p>
<p>主要原因是遗留技术的存在：</p>
<ol>
<li>当KVM产生一个虚拟机，它<span style="background-color: #c0c0c0;">期望一个TAP接口连接到虚拟机的以太网端口</span>（eth0），这样KVM就得到一个FD，它在其上读写以太网帧。也就是说<span style="background-color: #c0c0c0;">TAP是去不掉</span>的</li>
<li>而VETH是相对新的技术，能够支持Linux Bridge、命名空间、Open vSwitch等技术</li>
</ol>
<div class="blog_h2"><span class="graybg">MacVLAN</span></div>
<p>有时我们需要一块物理网卡<span style="background-color: #c0c0c0;">绑定多个 IP 以及多个 MAC 地址</span>，虽然<span style="background-color: #c0c0c0;">绑定多个 IP 很容易（通过网卡别名，例如eth0:1），但是这些 IP 会共享物理网卡的 MAC 地址</span>，可能无法满足我们的设计需求，所以有了 macvlan设备，其工作方式如下：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan.png"><img class="aligncenter size-full wp-image-26511" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan.png" alt="macvlan" width="690" height="322" /></a></p>
<p>配合网络命名空间机制，可以在<span style="background-color: #c0c0c0;">不需要建立Bridge</span>的情况下，为虚拟机建立独立的网络栈：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan-network-ns.png"><img class="aligncenter size-full wp-image-26517" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvlan-network-ns.png" alt="macvlan-network-ns" width="650" height="252" /></a></p>
<p>&nbsp;</p>
<p>MacVLAN接口可以看做是物理以太网接口的N个虚拟子接口（sub interface），每个子接口都具有区别于父接口（parent interface）的MAC地址，并且可以像普通网卡一样分配IP地址。这种<span style="background-color: #c0c0c0;">虚拟网卡在逻辑上，和物理网卡具有对等的地位</span>。</p>
<p>父接口可以是一个物理接口（例如eth0），可以是一个 802.1q 的子接口（例如eth0.10），也可以是 bonding 接口。</p>
<p>除了虚拟化之外，Keepalived也通过MacVLAN来使用虚拟Mac地址。</p>
<p>需要注意，使用MacVLAN的虚拟机/容器，<span style="background-color: #c0c0c0;">和主机共享一个网段</span>。如果虚拟机/容器<span style="background-color: #c0c0c0;">需要和宿主机通信，需要额外创建一个子接口给宿主机</span>使用</p>
<p>MacVLAN支持5种模式：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 13%; text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td class="blog_h3">bridge</td>
<td>
<p>类似于Linux Bridge，最常用。适合共享父接口的MacVLAN虚拟网卡直接进行通信的场景</p>
<p>这种模式下，共享父接口的虚拟网卡可以直接通信，不需要把流量发送到父接口外部</p>
<p>示例：</p>
<pre class="crayon-plain-tag"># 创建MacVLAN子接口，父接口为eth0，MAC地址默认自动分配，如需指定，可以 address 00:00:00:00:00:09
ip link add link eth0 name macv1 type macvlan mode bridge
ip link add link eth0 name macv2 type macvlan mode bridge

# if link可以看到新创建的两个子接口
5: macv1@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether a6:6d:2c:6e:33:bf brd ff:ff:ff:ff:ff:ff
6: macv2@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether 12:c9:36:fe:f8:6c brd ff:ff:ff:ff:ff:ff

# 移动到命名空间中
ip netns add net1
ip netns add net2
ip link set macv1 netns net1
ip link set macv2 netns net2

# 查看命名空间中的接口状态
ip netns exec net1 ip link
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# 类似于VETH对的一端，移动到命名空间后，使用@if2指明父接口在宿主机网络命名空间的序号
5: macv1@if2: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1
    link/ether a6:6d:2c:6e:33:bf brd ff:ff:ff:ff:ff:ff link-netnsid 0

# 配置IP地址
ip netns exec net1 ip addr add dev macv1 10.0.0.200/16
ip netns exec net2 ip addr add dev macv2 10.0.0.201/16
ip netns exec net1 ip link set dev macv1 up
ip netns exec net2 ip link set dev macv2 up

# 通信
ip netns exec net1 ping 10.0.0.1    # OK
ip netns exec net1 ping 10.0.0.201  # OK
ip netns exec net1 ping 10.0.0.200  # 失败
ip net exec net1 ip link set lo up  # 原因是命名空间中lo默认DOWN，导致不能PING自己</pre>
</td>
</tr>
<tr>
<td class="blog_h3">VEPA</td>
<td>
<p>Virtual Ethernet Port Aggregator，虚拟以太网端口聚合，这是默认的模式
<p>所有虚拟接口发出的流量，不管目的地是什么（即使是共享父接口的兄弟虚拟接口），都发给父接口</p>
<p>需要连接父接口们的交换机支持<span style="background-color: #c0c0c0;">Hairpin模式（可以把某个端口发出的包反射回去）</span>，把源、目的地址都是本地MacVLAN虚拟接口地址的流量，发给相应接口</p>
<p>大部分交换机设备不支持Hairpin模式，但是Linux Bridge可以支持：</p>
<pre class="crayon-plain-tag"># 对于网桥br0的端口eth0，从此端口收到的包，允许（可能经过某种处理后）从此端口再发回去
brctl hairpin br0 eth0 on
# or
ip link set dev eth0 hairpin on</pre>
</td>
</tr>
<tr>
<td class="blog_h3">Private</td>
<td>类似于VEPA，但是增强了隔离能力，阻止共享父接口的MacVLAN虚拟接口之间的通信</td>
</tr>
<tr>
<td class="blog_h3">Passthrough</td>
<td>这种模式下一个父接口只能和单个MacVLAN虚拟接口绑定，并且子接口继承父接口的MAC地址</td>
</tr>
<tr>
<td class="blog_h3">Source</td>
<td>MacVLAN接口只接受指定源MAC地址的数据包 </td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">跨主机通信</span></div>
<p>位于两台机器上的MacVLAN虚拟网卡要实现通信，需要满足：
<ol>
<li>两台机器的父接口都处于混杂模式</li>
<li>两台主机的MacVLAN子网IP段不重叠</li>
</ol>
<div class="blog_h3"><span class="graybg">和父接口通信</span></div>
<p>在MacVLAN的虚拟网络中，父接口仅仅相当于一个交换机。对于进出子MacVLAN接口的数据包，物理网卡仅仅转发而不处理，这导致使用本机MacVLAN网卡的IP无法和物理网卡的IP进行通信。</p>
<div class="blog_h3"><span class="graybg">缺点</span></div>
<p>MacVLAN是将虚拟机、容器连接到物理网络的简单方法。但是它有以下缺点：</p>
<ol>
<li>每个虚拟接口都需要MAC地址，但是物理交换器支持的MAC地址数量会有限制，许多物理网卡支持的MAC地址数量也会有限制</li>
<li>IEEE 802.11（WiFi）不支持一个客户端有多个MAC地址</li>
<li>在云上环境，VPC往往对入站报文的MAC地址进行校验，Linux Bridge面临同样的问题</li>
</ol>
<p>面临这些缺点时，可以考虑使用IPVLAN。</p>
<div class="blog_h2"><span class="graybg">IPVLAN</span></div>
<p>类似于MacVLAN，都是在一个父接口下虚拟出多个子接口。不同之处是，<span style="background-color: #c0c0c0;">IPVLAN的所有子接口MAC相同，仅仅IP不同</span>。</p>
<p>IPVLAN从3.19开始支持，<span style="background-color: #c0c0c0;">比较稳定的版本需要4.2+</span>。Docker对老版本的支持存在缺陷。</p>
<p>某些DHCP服务器在分配IP地址时，以MAC地址作为机器的标识，这和IPVLAN无法协同工作。</p>
<p>IPVLAN支持L2、L3等模式，<span style="background-color: #c0c0c0;">一个父接口同时只能在一种模式下工作</span>。</p>
<p><span style="background-color: #c0c0c0;">IPVLAN无法从宿主机访问子接口的IP地址，也不能从子接口所在命名空间访问父接口的IP地址</span>。你可以额外创建一对veth，配合路由，来解决这个问题。</p>
<div class="blog_h3"><span class="graybg">L2模式</span></div>
<p>这种模式下，和MacVLAN工作方式很类似。父接口作为交换机，转发子接口的数据。<span style="background-color: #c0c0c0;">子接口直接加入父接口所在的二层网络</span>。</p>
<p>实验：</p>
<pre class="crayon-plain-tag">ip link add link eth0 ipvlan1 type ipvlan mode l2
ip link add link eth0 ipvlan2 type ipvlan mode l2

ip net add net1
ip net add net2

ip link set ipvlan1 netns net1
ip link set ipvlan2 netns net2

ip net exec net1 ip link set ipvlan1 up
ip net exec net2 ip link set ipvlan2 up

ip net exec net1 ip addr add 10.0.10.1/16 dev ipvlan1
ip net exec net2 ip addr add 10.0.10.2/16 dev ipvlan2

ip net exec net1 route add default dev ipvlan1
ip net exec net2 route add default dev ipvlan2

ip net exec net1 ip link set ipvlan1 up
ip net exec net2 ip link set ipvlan2 up

ip net exec net1 ip link set lo up
ip net exec net2 ip link set lo up</pre>
<div class="blog_h3"><span class="graybg">L3模式</span></div>
<p>这种模式下， 父接口类似于路由器。它在各个<span style="background-color: #c0c0c0;">虚拟网络和主机网络之间</span>进行不同网络报文的<span style="background-color: #c0c0c0;">路由转发</span>工作。<span style="background-color: #c0c0c0;"> 只要父接口相同，即使虚拟机/容器不在同一个网络，也可以互相ping通对方</span>，因为ipvlan会在中间做报文的转发工作。 L3模式下的虚拟接口不会接收到多播或者广播的报文，所有的<span style="background-color: #c0c0c0;"> ARP 过程或者其他多播报文都是在底层的父接口</span>完成的。</p>
<p>外部网络不会理解IPVLAN虚拟出来的网络，因此如果外部路由器上不配置适当路由规则，IPVLAN的虚拟IP无法被外部访问。 </p>
<p>实验：</p>
<pre class="crayon-plain-tag">ip netns add net1
ip netns add net2

# 添加L3模式的IPVLAN子接口
ip link add link eth0 ipvlan1 type ipvlan mode l3
ip link add link eth0 ipvlan2 type ipvlan mode l3

# 可以发现子接口的MAC地址和父接口相同
2: eth0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff
5: ipvlan1@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff
6: ipvlan2@eth0: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN mode DEFAULT group default qlen 1000
    link/ether 10:00:00:00:00:07 brd ff:ff:ff:ff:ff:ff

# 移动到网络命名空间
ip link set ipvlan1 netns net1
ip link set ipvlan2 netns net2</pre>
<p>注意，由于父子接口的MAC地址一样，因此不能通过DHCP分配IP地址，必须手工添加：</p>
<pre class="crayon-plain-tag">ip net exec net1 ip addr add 10.98.0.1/16 dev ipvlan1
ip net exec net1 ip link set dev ipvlan1 up
ip net exec net2 ip addr add 10.99.0.1/16 dev ipvlan2
ip net exec net2 ip link set dev ipvlan2 up

# 添加路由
ip net exec net1 route add default dev ipvlan1
ip net exec net2 route add default dev ipvlan2

ip net exec net1 ping 10.99.0.1  # OK

# 在宿主机的L2网络的其它主机上，必须有路由才能访问IPVLAN子接口
#                                        IPVLAN父接口IP
ip route add 10.98.0.0/16 dev virbr0 via 10.0.0.7
ip route add 10.99.0.0/16 dev virbr0 via 10.0.0.7</pre>
<div class="blog_h3"><span class="graybg">L3S模式</span></div>
<p>类似于L3模式，但是L3S模式下出入流量均<span style="background-color: #c0c0c0;">经过宿主机网络命名空间的三层网络（L2/L3模式则不经过）</span>，会被宿主机的netfilter框架过滤。</p>
<p>这意味着L3S可以支持kube-proxy，但是，它又会引入以下问题：</p>
<ol>
<li>当K8S服务的客户端、Pod位于同一节点时，访问服务的应答报文会走ipvlan datapath，接收不到</li>
<li>同样场景下，同一方向流量多次进出宿主机 conntrack，datapath复杂，和iptables/ipvs也存在兼容性问题</li>
</ol>
<p>可以为容器额外引入VETH（另外一端放在宿主机），通过路由设置，让K8S服务的流量从VETH出站，解决Service无法访问的问题。</p>
<div class="blog_h2"><span class="graybg">MacVTap</span></div>
<p>Macvtap是一个新式驱动，用于简化虚拟化网络桥接。它基于macvlan设备驱动，将macvlan和TAP的优点进行整合。它使用macvlan的方式来收发数据包，但是收到的数据包<span style="background-color: #c0c0c0;">不会交给独立网络栈处理，而是交给/dev/tapX文件</span>：</p>
<p><a href="https://blog.gmem.cc/wp-content/uploads/2010/05/macvtap.png"><img class="aligncenter size-full wp-image-26521" src="https://blog.gmem.cc/wp-content/uploads/2010/05/macvtap.png" alt="macvtap" width="770" height="280" /></a></p>
<p>由于 macvlan是工作在 MAC 层的，所以 macvtap 也只能工作在 MAC 层，不会存在macvtun这样的设备。</p>
<p>一个macvtap端点（endpoint）是一个字符设备，它很大程度上遵循tun/tap ioctl接口，可以被KVM/QEMU或者其它支持tun/tap接口的hypervisors直接使用。<span style="background-color: #c0c0c0;">macvtap端点扩展一个既有网络接口（底层设备，lower device），具有自己的MAC地址</span>，位于与被扩展接口相同的网段。典型情况下，macvtap让宿主机、客户机一同出现在物理网络上。</p>
<div class="blog_h3"><span class="graybg">工作模式</span></div>
<p>类似于macvlan，一个macvtap可以工作在以下三种模式之一。这些模式定义了连接到同一lower device的macvtap端点之间的通信方式：</p>
<table class=" full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 15%; text-align: center;">模式</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>VEPA</td>
<td>
<p>即虚拟以太网端口聚合器（Virtual Ethernet Port Aggregator），这是默认的模式。从一个端点发送到另外一个端点的数据下发到lower device，进而发送到外部交换机，如果外部交换机支持hairpin模式，则以太网帧被发回lower device进而发送到目标端点。然而，大部分现代交换机都不支持hairpin模式，这意味着两个端点不能交换以太网帧（尽管它们可以通过TCP/IP路由器通信）</p>
</td>
</tr>
<tr>
<td>Bridge</td>
<td>直接连接端点，两个同时处于bridge模式的macvlan可以直接交换以太网帧。对于典型的交换机，这是最有用的模式</td>
</tr>
<tr>
<td>private</td>
<td>类似于VEPA，但是即使交换机支持hairpin，也被忽略，这种模式下端点之间绝不能相互通信</td>
</tr>
</tbody>
</table>
<div class="blog_h1"><span class="graybg">反向代理</span></div>
<p>亦称替代物（surrogate)，这类代理可以假扮Web服务器，接收发给 Web 服务器的真实请求，它可以向一个或者多个服务器索取内容，并返回给客户端，在客户端看来，好像反向代理就是真正的服务器。 反向代理有以下应用场景：</p>
<ol>
<li>反向代理可以缓存静态、动态内容，从而提高访问慢速Web服务器上公共内容时的性能，在此场景下反向代理被称为服务器加速器（Server accelerator）</li>
<li>隐藏原始服务器的存在和特性</li>
<li>为不支持SSL的服务器提供SSL支持</li>
<li>负载均衡：分散入站请求到多个服务器上</li>
<li>任何几个Web服务器需要通过同一IP地址访问的时候，可以将反向代理作为前置机</li>
</ol>
<p>&nbsp;</p>
<div class="blog_h1"><span class="graybg"><a id="transparent-proxy"></a>透明代理</span></div>
<p>所谓透明代理，它透明在：</p>
<ol>
<li>对于客户端，发起连接时连接的服务端是真实的服务器而不是代理服务器</li>
<li>对于服务器，收到的请求来自真实客户端而不是代理服务器</li>
</ol>
<p>必须具有某种机制，在客户端毫无知觉的情况下，将它的请求劫持给代理服务器处理。同样的，真实服务器发回的响应也需要直接发给代理服务器，而不是客户端。</p>
<p>实现透明代理，需要多方面的配合。客户端（C）、代理（P）、服务器（S）的部署架构会很大程度上影响技术实现。主要关注点包括：</p>
<ol>
<li>网络路由：目的地是S的IP包路由给P处理。如果S和P部署在一起（例如用于Sidecar部署的Envoy），通过Linux本身的能力就能达成</li>
<li>IP_TRANSPARENT：P必须启用此套接字选项</li>
</ol>
<div class="blog_h2"><span class="graybg">路由处理</span></div>
<p>当封包到达Linux内核时，它要么被路由，要么被丢弃，要么<span style="background-color: #c0c0c0;">被Linux本地处理 —— 如果目的地址匹配本地地址的话</span>。</p>
<p><span style="background-color: #c0c0c0;">什么是本地地址，是可以设置的</span>，甚至你可以设置为0.0.0.0/0 —— 任何地址都是本地地址，但这样设置会导致系统无法连接到任何远程地址，因为所有地址都是本地地址了…应当发给本地进程处理。幸运的是，<span style="background-color: #c0c0c0;">通过独立路由表，我们可以选择性的将一部分封包路由到本地</span>：</p>
<pre class="crayon-plain-tag"># 对于任何目的端口是53的封包，设置标记
iptables -t mangle -I PREROUTING -p udp --dport 5301 -j MARK --set-mark 1

# 然后配置路由表，强制将其从lo网卡发出，而不管目的地址是什么
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100 </pre>
<p>注意，需要启用IP转发功能：</p>
<pre class="crayon-plain-tag">sysctl net.ipv4.conf.all.forwarding=1
sysctl net.ipv6.conf.all.forwarding=1</pre>
<p>某些情况下反向路径过滤器（reverse path filter）可能会丢弃你拦截的封包，禁用之：</p>
<pre class="crayon-plain-tag">sysctl net.ipv4.conf.eth0.rp_filter=0</pre>
<div class="blog_h2"><span class="graybg">IP_TRANSPARENT</span></div>
<p>套接字选项IP_TRANSPARENT可以<span style="background-color: #c0c0c0;">绑定不属于本机的IP地址</span>，<span style="background-color: #c0c0c0;">P需要启用此选项</span>。此选项的作用：</p>
<ol>
<li>作为S的客户端时，此选项允许P使用不属于本机的IP地址作为源IP（也就是C的地址）发起连接</li>
<li>作为C的服务器时，可以侦听（绑定到）不属于本机的IP地址（也就是S的地址）的连接请求。比如开启此IP_TRANSPARENT选项同时监听0.0.0.0.80，那么本机接收到的DST地址是192.168.0.189:80的TCP请求可被监听</li>
<li>接受被TPROXY重定向的连接和封包</li>
</ol>
<p>下面是一段基于<a href="https://github.com/ahupowerdns/simplesocket">simplesocket</a>的代码，说明了IP_TRANSPARENT如何指定任意源地址的。注意需要CAP_NET_ADMIN权限才能运行：</p>
<pre class="crayon-plain-tag"># 基于TCP/IP协议族的UDP套接字 
Socket s(AF_INET, SOCK_DGRAM, 0);
# 套接字选项，IP_TRANSPARENT
SSetsockopt(s, IPPROTO_IP, IP_TRANSPARENT, 1);
# 使用的源地址
ComboAddress local("1.2.3.4", 5300);
# 使用的目的地址
ComboAddress remote("198.41.0.4", 53);
# 绑定到源地址，注意此地址不是本机地址 
SBind(s, local);
# 发送UDP包
SSendto(s, "hi!", remote);</pre>
<p>使用tcpdump可以发现：</p>
<pre class="crayon-plain-tag"># tcpdump -n host 1.2.3.4
# 源地址不是本机
21:29:41.005856 IP 1.2.3.4.5300 &gt; 198.41.0.4.53</pre>
<div class="blog_h2"><span class="graybg">TPROXY</span></div>
<p>Iptables支持一个特殊的目标TPROXY，它能够拦截流量，并将其转发给任意特定的本地IP地址，同时为封包设置标记：</p>
<pre class="crayon-plain-tag"># 修改针对25端口的TCP流量封包
iptables -t mangle -A PREROUTING -p tcp --dport 25 -j TPROXY
  # 设置0x1/0x1标记
  --tproxy-mark 0x1/0x1 
  # 转发给127.0.0.1:10025，但是不改变IP封包头的任何信息
  --on-port 10025      # 默认原始目的端口
  --on-ip 127.0.0.1    # 默认接收到流量的网卡地址</pre>
<p>注意：</p>
<ol>
<li>--tproxy-mark，它确保此封包通过正确的路由表发出。参考第一小节的路由，此封包  xxx:xxx:127.0.0.1:10025，将从lo网卡出网络栈</li>
<li>TPROXY和REDIRECT不同，后者本质上是DNAT，会改变IP封包头的目的地址</li>
</ol>
<div class="blog_h2"><span class="graybg">SocketMatch</span></div>
<p>Iptables匹配扩展socket能够<span style="background-color: #c0c0c0;">匹配和本地套接字相关的封包</span>，它会进行Socket Hash查找，并且在以下条件下匹配：</p>
<ol>
<li>本地存在已经建立的套接字，匹配此封包</li>
<li>如果本地存在非零监听套接字，并且地址端口匹配封包的地址端口</li>
</ol>
<p>示例：</p>
<pre class="crayon-plain-tag">iptables -t mangle -A PREROUTING -p tcp -m socket -j DIVERT

iptables -t mangle -N DIVERT
iptables -t mangle -A DIVERT -j MARK --set-mark 1
iptables -t mangle -A DIVERT -j ACCEPT

# 配合路由表</pre>
<div class="blog_h2"><span class="graybg">获取原始目的地址</span></div>
<p>使用IP_TRANSPARENT时：</p>
<ol>
<li>对于TCP连接，套接字的原始目的地址、端口可以通过getsockname()调用获得</li>
<li>对于UDP连接，你必须设置套接字选项IP_RECVORIGDSTADDR：<br />
<pre class="crayon-plain-tag">setsockopt (s, IPPROTO_IP, IP_RECVORIGDSTADDR, &amp;n, sizeof(int));</pre></p>
<p>然后，调用recvmsg()方法：</p>
<pre class="crayon-plain-tag">char orig_ip[32] = {0};
int orig_port = 0;
struct sockaddr_in *orig_addr;
for (cmsg = CMSG_FIRSTHDR(&amp;msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&amp;msg,cmsg)) {
    if (cmsg-&gt;cmsg_level == SOL_IP &amp;&amp; cmsg-&gt;cmsg_type == IP_ORIGDSTADDR) {
        orig_addr = (struct sockaddr_in *) CMSG_DATA(cmsg);
        transfer_sock_addr(orig_addr, orig_ip, 32, &amp;orig_port);
        break;
    }
}
if (cmsg == NULL) {
    printf("IP_ORIGDSTADDR not enabled or small buffer or I/O error");
    return;
} </pre>
</li>
</ol>
<div class="blog_h2"><span class="graybg">Istio的实例</span></div>
<p>参考<a href="/istio-study-note#tproxy-iptables-rules">Istio学习笔记</a>。
<div class="blog_h2"><span class="graybg">小结</span></div>
<ol>
<li>需要通过Iptables来对封包进行标记。时用TPROXY目标可以在标记的同时，转发给指定IP:PORT，但却又不改包头</li>
<li>需要为被标记的封包设置路由表，不管其目的地址是什么（0.0.0.0/0），总是发送到lo接口</li>
<li>lo接口接收到的封包，再次进入Iptables，这次不会走PREROUTING链</li>
<li>对于TPROXY，已经明确指定了接收者进程的IP:PORT，直接由此进程处理封包，前提是IP_TRANSPARENT被设置好</li>
<li>SocketMatch的作用是识别本地相关的套接字并标记，它不是必须的，TPROXY也能够做标记工作</li>
</ol>
<div class="blog_h1"><span class="graybg">环回</span></div>
<p>Loopback本来是通信学上的术语，和电信号、数字数据包的路由有关，将它们不做修改的路由给信号的发起者，主要用于测试通信基础设施。</p>
<div class="blog_h2"><span class="graybg">lo</span></div>
<p>在TCP/IP协议族的规范实现中，包含一个虚拟的网络接口，同一机器上的程序可以通过此虚拟接口进行网络通信，此接口完全是软件的，不会引起任何物理网卡的流量。程序发往lo接口的封包会简单的、<span style="background-color: #c0c0c0;">立即发回网络栈，就好像是从网络上收到的一样</span>。</p>
<p>如果路由将<span style="background-color: #c0c0c0;">出口网卡设置为lo（不管目的地址是什么），则报文交由本地的应用程序处理</span>，而不会真正路由出去。</p>
<div class="blog_h2"><span class="graybg">dummy</span></div>
<p>在Linux中，dummy类型的网络接口类似于lo，区别在于，你可以创建多个dummy类型的网络接口。</p>
<p>此外，对于lo接口，内核会自动添加目的地址是CIDR的路由：</p>
<pre class="crayon-plain-tag">local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1</pre>
<p> 而对于dummy接口则仅仅会添加针对其IP地址的路由。</p>
<div class="blog_h1"><span class="graybg">UDS</span></div>
<div class="blog_h2"><span class="graybg">简介</span></div>
<p>Socket API原本是为网络通讯设计的，但后来在socket的框架上发展出一种IPC机制，就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯（通过loopback地址127.0.0.1），但是<span style="background-color: #c0c0c0;">UNIX Domain Socket用于IPC更有效率：不需要经过网络协议栈，不需要打包拆包、计算校验和、维护序号和应答等</span>，只是将应用层数据<span style="background-color: #c0c0c0;">从一个进程拷贝到另一个进程</span>。这是因为，<span style="background-color: #c0c0c0;">IPC机制本质上是可靠的通讯</span>，而网络协议是为不可靠的通讯设计的。UNIX Domain Socket也提供<span style="background-color: #c0c0c0;">面向流和面向数据包两种API接口，类似于TCP和UDP</span>，但是面向消息的UNIX Domain Socke<span style="background-color: #c0c0c0;">t也是可靠</span>的，消息既不会丢失也不会顺序错乱。</p>
<p>UNIX Domain Socket是全双工的，API接口语义丰富，相比其它IPC机制有明显的优越性，目前已成为<span style="background-color: #c0c0c0;">使用最广泛的IPC机制</span>，比如X Window服务器和GUI程序之间就是通过UNIX Domain Socket通讯的。</p>
<p>使用UNIX Domain Socket的过程和网络socket十分相似，也要<span style="background-color: #c0c0c0;">先调用socket()创建一个socket文件描述符</span>，address family指定为AF_UNIX，type可以选择SOCK_DGRAM或SOCK_STREAM，protocol参数仍然指定为0即可。</p>
<p>UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同，用结构体sockaddr_un表示：</p>
<pre class="crayon-plain-tag">struct sockaddr_un {
    sa_family_t     sun_family;             /*PF_UNIX或AF_UNIX */
    char    sun_path[UNIX_PATH_MAX];        /* 路径名 */
}; </pre>
<p>网络编程的socket地址是IP地址加端口号，而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径，这个socket文件由bind()调用创建，如果<span style="background-color: #c0c0c0;">调用bind()时该文件已存在，则bind()错误返回</span>。</p>
<div class="blog_h2"><span class="graybg">抽象命名空间</span></div>
<p>所谓抽象套接字命名空间（Abstract Socket Namespace）是Linux的特性，用于创建一个<span style="background-color: #c0c0c0;">不去绑定到文件系统中文件的UDS</span>，从而获得以下好处：</p>
<ol>
<li>不需要担心文件系统中的命名冲突</li>
<li>当完成套接字使用后，不需要关注unlink文件路径名的问题 —— 到套接字关闭后，抽象名称会自动删除</li>
<li>不需要在文件系统中创建对应路径，在chroot环境下可能有用，在没有缺陷操控特定文件系统路径时也有用</li>
</ol>
<p>要创建抽象UDS套接字，需要指定sub_path的第一个字节为\0字符。在netstat lsof等命令的输出中，\0显示为@符号</p>
<div class="blog_h1"><span class="graybg">交换机</span></div>
<p>现代以太网依赖于交换机进行帧的交换。它通常是具有多个端口的硬件盒子。如果目的地址对于交换机未知（它不知道关联到哪个端口），那么交换机会广播帧给所有端口。通过观察端口上的帧流量，交换机就能学习到端口-MAC对应关系，从而不再需要广播流量。交换机在FIB（forwarding information base，也叫forwarding table）中缓存对应关系。</p>
<p>交换机可以相互连接，从而构成更大的以太网。</p>
<div class="blog_h1"><span class="graybg"><a id="route"></a>路由</span></div>
<div class="blog_h2"><span class="graybg">目的地类型</span></div>
<div class="blog_h3"><span class="graybg">本机地址</span></div>
<p>Linux支持loopback设备，用于通过网络栈和主机自身进行通信。Linux能够和任何配置在本地网络接口上的IP进行通信，不管地址是不是在loopback设备上。</p>
<p>所有配置在本机网络接口的地址，即本地地址（Locally Hosted Addresses）。</p>
<div class="blog_h3"><span class="graybg">本地可达地址</span></div>
<p>这类地址来自和本机位于同一网段内的其它主机。这些主机通过交换机相互连接，地址本地可达（Locally Reachable Addresses）</p>
<div class="blog_h3"><span class="graybg">其它地址</span></div>
<p>所有其它地址，都必须依赖路由器（IP Routing Device）作为中介才能到达，并且，此路由器具有本地可达地址。</p>
<div class="blog_h2"><span class="graybg">跳/Hop</span></div>
<p>当封包从一个网络（经过路由器）到达另外一个网络，称为经过了一跳。跳的数量，即封包经过的中介路由器的数量。</p>
<div class="blog_h3"><span class="graybg">下一跳</span></div>
<p>对于一个路由路径，封包要经过的下一个网关（路由器），即下一跳。</p>
<div class="blog_h2"><span class="graybg">路由器</span></div>
<p>任何在两个网络之间接受/转发封包的设备，都是路由器。路由器至少是dual-homed的 —— 每个接口接入到一个网络，网络接口通常是NIC。Linux主机经常扮演路由器的角色，NIC经常是VLAN接口之类的虚拟设备。</p>
<div class="blog_h2"><span class="graybg">默认路由</span></div>
<p>默认路由，即目的地是0/0的路由，是最一般的路由——如果找不到和封包目的地更匹配的路由，则自动使用默认路由。</p>
<p>对于连接到因特网的主机来说，默认路由通常是本地可达的路由器，此路由器也叫默认网关。</p>
<div class="blog_h2"><span class="graybg">路由选择</span></div>
<p>路由选择的基本原则是逐跳（hop-by-hop）进行，<span style="background-color: #c0c0c0;">典型情况下以目的地址为选路的唯一标准</span>。Linux作为路由器时，可能<span style="background-color: #c0c0c0;">基于其它的封包特征进行选路，此所谓策略路由</span>。</p>
<p>基于目的地址选路时，<span style="background-color: #c0c0c0;">首先会查询路由缓存，然后查找主路由表</span>。如果<span style="background-color: #c0c0c0;">最近发送封包到了目的地址，则关于目的地址的路由会存在于缓存（本质上是一个hash表）</span>中。如果最近没有发送封包到目的地，则需要查询路由表，匹配的原则是最长前缀匹配（longest prefix match） —— 也就是尽量选择精确的路由。最长前缀匹配能够针对大规模网络的路由规则，被<span style="background-color: #c0c0c0;">更加精确的路由器（通常更靠近）或主机覆盖</span>。</p>
<div class="blog_h2"><span class="graybg">策略路由</span></div>
<p>从2.2开始，Linux支持基于多路由表（multiple routing tables）和路由策略数据库（routing policy database，RPDB）的策略路由。</p>
<p>策略路由的依据，是封包的某些属性，例如源地址、ToS标记、fwmark（仅在内核中存在的、位于表示封包的数据结构中的字段）、接收封包接口名，等等。</p>
<p>启用策略路由的情况下，选路算法如下：</p>
<ol>
<li>首先，仍然是查询路由缓存</li>
<li>基于优先级，来遍历RPDB。查找匹配的RPDB</li>
<li>对于每个匹配RPDB条目，使用最长前缀原则匹配封包目的地址，遍历条目对应的路由表</li>
<li>如果找到匹配路由，则终止迭代</li>
</ol>
<p>伪代码：</p>
<pre class="crayon-plain-tag"># 首先查找路由缓存
if packet.routeCacheLookupKey in routeCache :
    route = routeCache[ packet.routeCacheLookupKey ]
else
    # 然后按优先级遍历RPDB，此数据库中是一条条规则
    for rule in rpdb :
        # 如果封包匹配RPDB规则
        if packet.rpdbLookupKey in rule :
            # 则遍历RPDB规则对应的路由表
            routeTable = rule[ lookupTable ]
            # 根据最长前缀原则来匹配路由表中的路由
            if packet.routeLookupKey in routeTable :
                route = route_table[ packet.routeLookup_key ]</pre>
<p><span style="background-color: #c0c0c0;">使用策略路由时，单个路由表的使用逻辑保持不变</span>。</p>
<div class="blog_h2"><span class="graybg">路由缓存</span></div>
<p>路由缓存，又叫转发信息库（forwarding information base，FIB）。它是一个存放了最近使用的路由条目的哈希表，在查询路由表之前，会优先检查FIB。</p>
<p>FIB由内核独立于路由表维护，修改路由表可能不会立即反应到FIB，要避免此延迟，可以调用：</p>
<pre class="crayon-plain-tag">ip route flush cache</pre>
<p>FIB清空后，新的封包，或者<pre class="crayon-plain-tag">ip route get</pre>命令会触发路由表查找、重新填充缓存。 </p>
<p>FIB提供基于多种方式的哈希查找，包括：dst（目的地址）、src（源地址）、tos（服务类型）、fwmark（内核封包标记）、iif（入站网络接口）。</p>
<p>FIB的缓存条目上，存储了以下属性：cwnd、advmss（建议最大段大小）、src（Preferred Local源地址）、mtu、rtt、age、users、used。</p>
<div class="blog_h2"><span class="graybg">路由表</span></div>
<p>Linux 2.2 / 2.4支持多路由表，除了广泛使用的local / main表外，内核最多支持额外的252张路由表。联合多个路由表（主要基于目的地址）和RPDB（主要基于源地址），Linux内核实现了灵活的路由功能。</p>
<p>支持多重路由表的内核，使用0-255之间的整数来引用路由表。最基本的路由表是local（255）、main（252），路由表序号和名字的映射关系定义在<pre class="crayon-plain-tag">/etc/iproute2/rt_tables</pre>中。</p>
<p>下面的命令显示所有路由表的路由：</p>
<pre class="crayon-plain-tag">ip route show table all</pre>
<p>下面的命令显示特定路由表中的路由：</p>
<pre class="crayon-plain-tag">ip route show table main
ip route show table 2005</pre>
<div class="blog_h3"><span class="graybg">local表</span></div>
<p>这是一张<span style="background-color: #c0c0c0;">由内核维护的特殊表</span>，条目可以删除，但是需要注意风险。ip address和ifconfig命令会导致内核修改local表（通常也同时修改main表）。</p>
<p>local表的主要用途有两个：</p>
<ol>
<li>指定广播地址的规格，仅仅对于支持广播寻址的L2有用</li>
<li>用于<span style="background-color: #c0c0c0;">路由到本机IP地址</span></li>
</ol>
<p>local表中可以出现的路由类型包括：local、nat、broadcast。这些路由类型不会出现在其它表，其它路由类型不会出现在local表。</p>
<p>如果某个<span style="background-color: #c0c0c0;">网络接口具有多个IP地址，则每个IP地址都在local表中具有一个路由条目</span>。这是在Linux下为网络接口添加IP地址的正常附加效果。</p>
<div class="blog_h3"><span class="graybg">main表</span></div>
<p>命令route操控的是该表，如果ip route不指定目标表，则默认操作main表。</p>
<div class="blog_h2"><span class="graybg">路由条目</span></div>
<p>路由表可以包含任意多个（除了local表，它由内核维护）条目。</p>
<p>条目的形式为：</p>
<pre class="crayon-plain-tag">ip route { add | del | change | append | replace } ROUTE

ROUTE := NODE_SPEC [ INFO_SPEC ]
  #            路由类型  匹配CIDR           路由表              路由协议           范围
  NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ] [ table TABLE_ID ] [ proto RTPROTO ] [ scope SCOPE ] [ metric METRIC ]
    TYPE := [ unicast | local | broadcast | multicast | throw | unreachable | prohibit | blackhole | nat ]
    TABLE_ID := [ local| main | default | all | NUMBER ]
    RTPROTO := [ kernel | boot | static | NUMBER ]
    SCOPE := [ host | link | global | NUMBER ]

  INFO_SPEC := NH OPTIONS FLAGS [ nexthop NH ] ...
    #                     下一跳路由器地址             出口网卡名称    权重，在multipath路由中反应此路由的相对带宽或质量 
    NH := [ encap ENCAP ] [ via [ FAMILY ] ADDRESS ] [ dev STRING ] [ weight NUMBER ] NHFLAGS
      FAMILY := [ inet | inet6 | ipx | dnet | mpls | bridge | link ]
      NHFLAGS := [ onlink | pervasive ]
    OPTIONS := FLAGS [ mtu NUMBER ] [ advmss NUMBER ] [ as [ to ] ADDRESS ] rtt TIME ] [ rttvar TIME ] 
                     [ reordering NUMBER ] [ window NUMBER ] [ cwnd NUMBER ] [ ssthresh REALM ] [ realms REALM ] 
                     [ rto_min TIME ] [ initcwnd NUMBER ] [ initrwnd NUMBER ] [ features FEATURES ] [ quickack BOOL ] 
                     [ congctl NAME ] [ pref PREF ] [ expires TIME ]</pre>
<div class="blog_h3"><span class="graybg">路由类型</span></div>
<p>路由类型可以分为一下几种：</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">类型</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>unicast</td>
<td>单播，最常见的路由条目，针对特定单一目的主机：<br />
<pre class="crayon-plain-tag">ip route add unicast 192.168.0.0/24 via 192.168.100.5
ip route add default via 193.7.255.1
ip route add unicast default via 206.59.29.193
ip route add 10.40.0.0/16 via 10.72.75.254</pre></p>
<p>如果ip route命令不指定路由条目类型，则默认是unicast </p>
</td>
</tr>
<tr>
<td>broadcast</td>
<td>用于支持广播地址的链路层设备，仅用在local表中，通常由内核管理：<br />
<pre class="crayon-plain-tag">ip route add table local broadcast 10.10.20.255 dev eth0 proto kernel scope link src 10.10.20.67 </pre>
</td>
</tr>
<tr>
<td>local</td>
<td>每当一个<span style="background-color: #c0c0c0;">IP地址添加到网络接口上时，内核自动在local表添加local类型的路由条目</span>：<br />
<pre class="crayon-plain-tag">ip route add 
  # 添加到local表    路由类型为local
  table local       local 
  # PREFIX，不带 / 为单一地址
  10.96.0.1 
  # 出口设备名称
  dev kube-ipvs0 
  # 内核自动添加的条目
  proto kernel 
  # 主机范围
  scope host 
  # 优先使用的源地址
  src 10.96.0.1</pre></p>
<p> 例如上面这个条目，是K8S的Kube Proxy IPVS模式下为kube-ipvs0配置10.96.0.1地址后，内核自动增加的条目</p>
</td>
</tr>
<tr>
<td>nat</td>
<td>当尝试配置无状态NAT时，内核添加这类条目到local表：<br />
<pre class="crayon-plain-tag">ip route add nat 193.7.255.184 via 172.16.82.184
ip route add nat 10.40.0.0/16 via 172.40.0.0</pre>
</td>
</tr>
<tr>
<td>unreachable</td>
<td>当匹配这类路由条目时，会返回ICMP unreachable消息：<br />
<pre class="crayon-plain-tag">ip route add unreachable 192.168.14.0/26 </pre>
</td>
</tr>
<tr>
<td>prohibit</td>
<td>当匹配这类路由条目时，会返回ICMP prohibited消息</td>
</tr>
<tr>
<td>blackhole</td>
<td>当匹配这类路由条目时，封包被丢弃，不会产生ICMP消息<br />
<pre class="crayon-plain-tag">ip route add blackhole 64.65.64.0/18 </pre>
</td>
</tr>
<tr>
<td>throw</td>
<td>
<p>导致针对当前路由表的查找立即失败，返回RPDB（可能继续匹配后续规则并查找相应的其它路由表）</p>
<pre class="crayon-plain-tag">ip route add throw 10.79.0.0/16</pre>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">路由SCOPE</span></div>
<p>所谓route scope，是一个“指示符”，用来说明到目标网络的“距离”。 
<p>必须是定义在  /etc/iproute2/rt_scopes 中的名字或数字</p>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>global<br />universe</td>
<td>
<p>此Scope提示目的地远于一跳</p>
<p>对于所有通过网关（gatewayed）的单播路由，这是默认值</p>
</td>
</tr>
<tr>
<td>link</td>
<td>
<p>此Scope提示目的地在本地网络上</p>
<p>对于直接单播路由、广播路由，这是默认值</p>
</td>
</tr>
<tr>
<td>host</td>
<td>
<p>此Scope提示目的地在本机上</p>
<p>对于local类型路由，这是默认值</p>
</td>
</tr>
</tbody>
</table>
<div class="blog_h3"><span class="graybg">路由协议标识符</span></div>
<table class="full-width fixed-word-wrap">
<thead>
<tr>
<td style="width: 150px; text-align: center;">取值</td>
<td style="text-align: center;">说明</td>
</tr>
</thead>
<tbody>
<tr>
<td>redirect</td>
<td>因为ICMP重定向而安装此路由条目</td>
</tr>
<tr>
<td>kernel</td>
<td>因为内核执行指定配置而安装此路由条目</td>
</tr>
<tr>
<td>boot</td>
<td>路由条目在启动期间安装，当路由守护进程启动后，会清除所有这类条目</td>
</tr>
<tr>
<td>static</td>
<td>管理员手工添加，用于覆盖动态路由。路由守护进程会遵从此条目，甚至同步给它的peers</td>
</tr>
<tr>
<td>ra</td>
<td>由路由发现协议安装</td>
</tr>
</tbody>
</table>
<div class="blog_h2"><span class="graybg">路由策略数据库</span></div>
<p>RPDB控制内核遍历多张路由表的顺序。RPDB中的每个条目，具有一个优先级，优先级是0-32767之间的数字，越小优先级越高。下面是一个示例的路由策略：</p>
<pre class="crayon-plain-tag">ip rule list
9:      from all fwmark 0x200/0xf00 lookup 2004 
10:     from all fwmark 0xa00/0xf00 lookup 2005 
100:    from all lookup local 
32766:  from all lookup main 
32767:  from all lookup default</pre>
<p>当封包达到后，假设没有路由缓存，内核会首先检查优先级为0的RPDB规则，如果匹配，则使用该规则指向的路由表，如果不匹配，则依次检查低优先级的规则。</p>
<p>RPDB中的规则类型有若干种，和路由类型对应：</p>
<pre class="crayon-plain-tag"># unicast 这是最常见的规则类型，也是默认的规则类型。会导致查找某个路由表
# 如果源地址是192.168.100.17则查找表5
ip rule add unicast from 192.168.100.17 table 5
# 如果源接口是eth7则查找表5
ip rule add unicast iif eth7 table 5
# 如果封包具有标记4则查找4
ip rule add unicast fwmark 4 table 4


# nat 用于正确实现无状态NAT，通常和nat类型的路由条目耦合在一起。这种条目
# 会导致内核重写出站封包的源地址
#  对于源地址是172.16.82.184的封包，将源地址改写为193.7.255.184
ip rule add nat 193.7.255.184 from 172.16.82.184
ip rule add nat 10.40.0.0 from 172.40.0.0/16


# unreachable 如果匹配，则立即应答 ICMP unreachable 给源地址
# 如果是来自eth2的、服务类型0xc0的封包
ip rule add unreachable iif eth2 tos 0xc0
ip rule add unreachable iif wan0 fwmark 5
ip rule add unreachable from 192.168.7.0/25


# prohibit 如果匹配，则立即应答 ICMP prohibit 给源地址
ip rule add prohibit from 209.10.26.51
ip rule add prohibit to 64.65.64.0/18
ip rule add prohibit fwmark 7


# blackhole 如果匹配，封包被静默的丢弃
ip rule add blackhole from 209.10.26.51
ip rule add blackhole from 172.19.40.0/24
ip rule add blackhole to 10.182.17.64/28</pre>
<div class="blog_h2"><span class="graybg">源地址选择</span></div>
<p>对于具有多重IP地址的主机来说，进行相互通信时必须选择正确的源IP地址。</p>
<p>出站封包的源地址选择的原则如下：</p>
<ol>
<li>如果应用程序已经在使用套接字，则源地址已经被选择过</li>
<li>应用程序<span style="background-color: #c0c0c0;">可以显式请求一个源地址，这个地址甚至可以不是本机地址。</span>很多应用程序支持选取源地址，例如：<br />
<pre class="crayon-plain-tag">nc -s $BINDADDR $DEST $PORT
socat - TCP4:$REMOTEHOST:$REMOTEPORT,bind=$BINDADDR </pre>
</li>
<li>内核进行选路操作，如果<span style="background-color: #c0c0c0;">匹配路由存在src参数，则内核会利用该参数作为源地址</span></li>
<li>如果没有src提示，则内核会根据目的地址，找到本机上<span style="background-color: #c0c0c0;">第一个配置了与该地址在同一个网段、或者与该目的地址的路由下一跳地址在同一网段的IP地址的网络接口，并用此网络接口的IP地址</span>作为源地址</li>
</ol>
<div class="blog_h2"><span class="graybg">ICMP和路由</span></div>
<div class="blog_h3"><span class="graybg">PMTU</span></div>
<p>Path MTU即（Path Maximum Transmission Unit），ICMP报文可以用于发现PMTU，它是整个路由路径中，最小的MTU。知道这个值以后，可以避免不必要的IP分片。</p>
<p>路由中任何一跳阻止ICMP报文，都会导致无法识别PMTU。</p>
<div class="blog_h3"><span class="graybg">ICMP重定向</span></div>
<p>ICMP重定向是路由器提示发送者，具有更好路由路径的一种机制。配置静态路由可以防止不期望的ICMP重定向，尽管ICMP重定向不会导致危险，但是在良好管理的网络中是不应当发生的。</p>
<div class="blog_h2"><span class="graybg"><a id="ecmp"></a>ECMP</span></div>
<p>ECMP是一个逐跳的基于流的负载均衡策略，当路由器发现<span style="background-color: #c0c0c0;">同一目的地址出现多个最优路径时，会更新路由表，为此目的地址添加多条规则，对应于多个下一跳</span>。ECMP的路径选择策略有多种方法：</p>
<ol>
<li>哈希，例如根据源IP地址的哈希为流选择路径</li>
<li>轮询，各个流在多条路径之间轮询传输</li>
<li>基于路径权重，根据路径的权重分配流，权重大的路径分配的流数量更多</li>
</ol>
<p>OSPF、ISIS、EIGRP、BGP等多种路由协议均支持ECMP。</p>
<p>ECMP是一种较为简单的负载均衡策略，其在实际使用中面临的问题也不容忽视：</p>
<ol>
<li>可能增加链路的拥塞：ECMP并没有拥塞感知的机制，只是将流分散到不同的路径上转发。对于已经产生拥塞的路径来说，很可能加剧路径的拥塞</li>
<li>非对称网络使用效果不好</li>
</ol>
<div class="blog_h1"><span class="graybg">防火墙</span></div>
<div class="blog_h2"><span class="graybg">iptables</span></div>
<p>参考：<a href="/iptables">重温iptables</a></p>
<div class="blog_h1"><span class="graybg">流量控制</span></div>
<div class="blog_h3"><span class="graybg">tc</span></div>
<p>参考<a href="/tc">基于tc的网络QoS管理</a></p>
<div class="blog_h1"><span class="graybg">Offloading</span></div>
<p>Offloading是Linux内核中一系列用于减轻CPU网络处理负担的技术。</p>
<p>随着技术的进步，网络接口的带宽越来越大。从早期的10Mbps发展到现在的10Gbps+，带宽增加了3个数量级。与此同时，网络中能够传递的单个封包的大小，受限于路径MTU。在简单的局域网环境下，可以使用Jumbo Frame将MTU提升到9KB，但是在复杂的因特网环境下，MTU一直都只能是1500左右。作为后果，带宽打满的情况下CPU单位时间需要处理的封包个数比早期多了3个数量级。现代处理器在处理1Gbps网络接口时，通常能够应对，不需要考虑Offloading。</p>
<p>尽管这些年CPU的性能也有很大的提升，包括频率升高和核心数增加，但是减少需要处理的封包数量对于提升性能仍然是很有价值的。每个封包都需要经过网络栈，处理过程相当复杂。</p>
<div class="blog_h2"><span class="graybg">TSO</span></div>
<p>TCP Segmentation Offload，允许内核发送一个很大的封包，例如64KB，然后由（支持TSO的）网络适配器将封包分割为合适大小的TCP段传输。</p>
<p>TSO能够平均减少发送单个封包的开销达40倍，对于以发送为主的工作负载，例如下载服务器，足以让10Gbps网络全速工作。</p>
<div class="blog_h2"><span class="graybg">GSO</span></div>
<p>Generic Segmentation Offload，更加一般的segmentation offloading机制，可以用于UDP协议。</p>
<p>即使在驱动层模拟GSO，也能提升性能。</p>
<div class="blog_h2"><span class="graybg">LRO</span></div>
<p>在接收端进行Offloading的技术出现较晚，一方面早期的网络流量均是下行为主，另一方面接收端的处理难度要大的多。你无法控制什么时候接收封包，这是由对端控制的。</p>
<p>Large Receive Offload类似于GSO，它允许网络接口将接收到的封包进行合并，让操作系统看到的封包变少。即使在驱动层模拟LRO，也能提升性能。LRO被Linux中10Gbps网卡驱动广泛的支持。LRO仅仅支持TCP/IPv4。</p>
<p>LRO存在缺陷，它只是简单的将看到的封包都合并起来，如果包头中有差异这些差异会丢失。这些信息的丢失会导致问题：</p>
<ol>
<li>如果Linux主机作为路由器运行，那么它任何时候都不应该改变包头中的信息</li>
<li>某些基于卫星网络的连接，依赖于特殊的头才能正常运作</li>
<li>Linux网桥无法工作，导致很多虚拟化场景无法使用LRO</li>
</ol>
<div class="blog_h2"><span class="graybg">GRO</span></div>
<p>Generic Receive Offload，解决了LRO的缺陷。GRO严格限制了封包合并的条件：</p>
<ol>
<li>MAC地址必须相同</li>
<li>仅允许很少一部分的TCP或IP头不同</li>
</ol>
<p>这些限制让合并后的封包能够被无损的重新分段。<span style="color: #000000;">GRO code可以用于重新分段。</span></p>
<p>GRO不限制于TCP协议。</p>
</div><p>The post <a rel="nofollow" href="https://blog.gmem.cc/network-faq">Linux网络知识集锦</a> appeared first on <a rel="nofollow" href="https://blog.gmem.cc">绿色记忆</a>.</p>
]]></content:encoded>
			<wfw:commentRss>https://blog.gmem.cc/network-faq/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
