目录

1. Socket 概述

2. LwIP 中的socket

3. Socket API

3.1 socket()

3.2 bind()

3.3 connect()

3.4 listen()

3.5 accept()

3.6 read()、recv()、recvfrom()

3.7 sendto()

3.8 send()

3.9 write()

3.10 close()

3.11 ioctl()、ioctlsocket()

3.12 setsockopt()

3.13 getsockopt()

4. 实验例程

4.1  TCP Server

4.2  TCP Client


1. Socket 概述

Socket 英文原意是“孔”或者“插座”的意思,在网络编程中,通常将其称之为“套接字”,当前网络中的主流程序设计都是使用Socket 进行编程的,因为它简单易用,更是一个标准,能在不同平台很方便移植。

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层连接唯一地被通信两端的两个端点(即两个套接字)所确定。

Socket最初是加利福尼亚大学Berkeley分校为Unix系统开发的网络通信接口。后来随着TCP/IP网络的发展,Socket成为最为通用的应用程序接口,也是在Internet上进行应用开发最为通用的API。

为了能让更多开发者直接上手LwIP 的编程,专门设计了LwIP 的第三种编程接口——Socket API,它兼容BSD Socket。

Socket 虽然是能在多平台移植,但是LwIP 中的Socket 并不完善,因为LwIP 设计之初就是为了在嵌入式平台中使用,它只实现了完整Socket 的部分功能,不过,在嵌入式平台中,这些功能早已足够。

2. LwIP 中的socket

在LwIP 中,Socket API 是基于NETCONN API 之上来实现的,系统最多提供MEMP_NUM_NETCONN 个netconn 连接结构,因此决定Socket 套接字的个数也是那么多个。

为了更好对netconn 进行封装,LwIP 还定义了一个套接字结构体——lwip_sock(称之为Socket 连接结构),每个lwip_sock 内部都有一个netconn 的指针,实现了对netconn 的再次封装。

LwIP 定义了一个lwip_sock 类型的sockets数组,通过套接字就可以直接索引并且访问这个结构体了,这也是为什么套接字是一个整数的原因,lwip_sock 结构体是比较简单的,因为基本上全是依赖netconn 实现。

#define NUM_SOCKETS MEMP_NUM_NETCONN    // 默认是4/** 全局可用套接字数组 **/
static struct lwip_sock sockets[NUM_SOCKETS];union lwip_sock_lastdata {struct netbuf *netbuf;struct pbuf *pbuf;
};/** 包含用于套接字的所有内部指针和状态*/
struct lwip_sock {/** 套接字当前是在netconn 上构建的,每个套接字都有一个netconn*/struct netconn *conn;/** 从上一次读取中留下的数据 */union lwip_sock_lastdata lastdata;
#if LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL/** number of times data was received, set by event_callback(),tested by the receive and select functions */s16_t rcvevent;/** number of times data was ACKed (free send buffer), set by event_callback(),tested by select */u16_t sendevent;/** error happened for this socket, set by event_callback(), tested by select */u16_t errevent;/** 使用select 等待此套接字的线程数 */SELWAIT_T select_waiting;
#endif /* LWIP_SOCKET_SELECT || LWIP_SOCKET_POLL */
#if LWIP_NETCONN_FULLDUPLEX/* counter of how many threads are using a struct lwip_sock (not the 'int') */u8_t fd_used;/* status of pending close/delete actions */u8_t fd_free_pending;
#define LWIP_SOCK_FD_FREE_TCP  1
#define LWIP_SOCK_FD_FREE_FREE 2
#endif
};

3. Socket API

3.1 socket()

向内核申请一个套接字,在本质上该函数其实就是对netconn_new()函数进行了封装,虽然说不是直接调用它,但是主体完成的工作就做了 netconn_new()函数的事情,而且该函数本质是一个宏定义.

/** @ingroup socket */
#define socket(domain,type,protocol)              lwip_socket(domain,type,protocol)int
lwip_socket(int domain, int type, int protocol);#define AF_INET         2/* Socket protocol types (TCP/UDP/RAW) */
#define SOCK_STREAM     1
#define SOCK_DGRAM      2
#define SOCK_RAW        3

参数domain :表示该套接字使用的协议簇,对于TCP/IP 协议来说,该值始终为AF_INET。

参数type: 指定了套接字使用的服务类型,可能的类型有3 种:

1. SOCK_STREAM:提供可靠的(即能保证数据正确传送到对方)面向连接的Socket 服务,多用于资料(如文件)传输,如TCP 协议。

2. SOCK_DGRAM:是提供无保障的面向消息的Socket 服务,主要用于在网络上发广播信息,如UDP 协议,提供无连接不可靠的数据报交付服务。

3. SOCK_RAW:表示原始套接字,它允许应用程序访问网络层的原始数据包,这个套接字用得比较少,暂时不用理会它。

参数protocol: 指定了套接字使用的协议,在IPv4 中,只有TCP 协议提供SOCK_STREAM这种可靠的服务,只有UDP 协议供SOCK_DGRAM服务,对于这两种协议,protocol 的值均为0。

当申请套接字成功的时候,该函数返回一个int 类型的值,也是Socket 描述符,用户通过这个值可以索引到一个Socket 连接结构——lwip_sock,当申请套接字失败时,该函数返回-1。

3.2 bind()

该函数的功能与netconn_bind()函数是一样的,用于服务器端绑定套接字与网卡信息,实际上就是对netconn_bind()函数进行了封装,可以将一个申请成功的套接字与网卡信息进行绑定。

/** @ingroup socket */
#define bind(s,name,namelen)                      lwip_bind(s,name,namelen)int
lwip_bind(int s, const struct sockaddr *name, socklen_t namelen);

参数s : 表示要绑定的Socket 套接字

参数name: 是一个指向sockaddr 结构体的指针,其中包含了网卡的IP 地址、端口号等重要的信息,LwIP 为了更好描述这些信息,使用了sockaddr 结构体来定义了必要的信息的字段,它常被用于Socket API 的很多函数中,我们在使用bind()的时候,只需要直接填写相关字段即可.

参数namelen: 指定了name 结构体的长度

struct sockaddr {u8_t        sa_len;            /* 长度 */sa_family_t sa_family;         /* 协议簇 */char        sa_data[14];       /* 连续的14字节信息 */
};

需要填写的IP 地址与端口号等信息,都在sa_data 连续的14 字节信息里面,但是这个数据对我们不友好,因此LwIP 还定义了另一个对开发者更加友好的结构体——sockaddr_in,我们一般也是用这个结构体.

/* members are in network byte order */
struct sockaddr_in {u8_t            sin_len;        // 长度sa_family_t     sin_family;     // 协议簇  uint8_tin_port_t       sin_port;       // 端口    uint16_tstruct in_addr  sin_addr;       // 地址    uint32_t
#define SIN_ZERO_LEN 8char            sin_zero[SIN_ZERO_LEN];
};

这个结构体的前两个字段是与sockaddr 结构体的前两个字段一致,而剩下的字段就是sa_data 连续的14 字节信息里面的内容,只不过从新定义了成员变量而已,sin_port 字段是我们需要填写的端口号信息,sin_addr 字段是我们需要填写的IP 地址信息,剩下sin_zero区域的8 字节保留未用.

使用例程:

#define PORT              5001
#define IP_ADDR        "192.168.0.181"int sock = -1;
struct sockaddr_in server_addr;sock = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = inet_addr(IP_ADDR);
memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));    if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1) {;
}

3.3 connect()

函数的作用与netconn_connect()函数的作用基本一致,因为就是封装了netconn_connect()函数。它用于客户端中,将Socket 与远端IP 地址、端口号进行绑定,在TCP 客户端连接中,调用这个函数将发生握手过程(会发送一个TCP 连接请求),并最终建立新的TCP 连接,而对于UDP 协议来说,调用这个函数只是在UDP 控制块中记录远端IP 地址与端口号,而不发送任何数据,参数信息与bind()函数是一样的.

/** @ingroup socket */
#define connect(s,name,namelen)                   lwip_connect(s,name,namelen)int
lwip_connect(int s, const struct sockaddr *name, socklen_t namelen);

3.4 listen()

函数是对netconn_listen()函数的封装,只能在TCP 服务器中使用,让服务器进入监听状态,等待远端的连接请求,LwIP 中可以接收多个客户端的连接,因此参数backlog 指定了请求队列的大小.

/** @ingroup socket */
#define listen(s,backlog)                         lwip_listen(s,backlog)int
lwip_listen(int s, int backlog);

3.5 accept()

accept()函数与netconn_accept()函数作用一样,用于TCP 服务器中,等待着远端主机的连接请求,并且建立一个新的TCP 连接,在调用这个函数之前需要通过调用listen()函数让服务器进入监听状态。accept()函数的调用会阻塞应用线程直至与远程主机建立TCP 连接。参数addr 是一个返回结果参数,它的值由accept()函数设置,其实就是远程主机的地址与端口号等信息,当新的连接已经建立后,远端主机的信息将保存在连接句柄中,它能够唯一的标识某个连接对象。同时函数返回一个int 类型的套接字描述符,根据它能索引到连接结构,如果连接失败则返回-1.

/** @ingroup socket */
#define accept(s,addr,addrlen)                    lwip_accept(s,addr,addrlen)int
lwip_accept(int s, struct sockaddr *addr, socklen_t *addrlen);

3.6 read()、recv()、recvfrom()

read()与recv()函数的核心是调用recvfrom()函数,而recvfrom()函数是基于netconn_recv()函数来实现的,recv()与read()函数用于从Socket 中接收数据,它们可以是TCP 协议和UDP 协议

/** @ingroup socket */
#define read(s,mem,len)                           lwip_read(s,mem,len)ssize_t
lwip_read(int s, void *mem, size_t len)
{return lwip_recvfrom(s, mem, len, 0, NULL, NULL);
}/** @ingroup socket */
#define recv(s,mem,len,flags)                     lwip_recv(s,mem,len,flags)ssize_t
lwip_recv(int s, void *mem, size_t len, int flags)
{return lwip_recvfrom(s, mem, len, flags, NULL, NULL);
}ssize_t
lwip_recvfrom(int s, void *mem, size_t len, int flags,struct sockaddr *from, socklen_t *fromlen);

men 参数记录了接收数据的缓存起始地址,

len 用于指定接收数据的最大长度,如果函数能正确接收到数据,将会返回一个接收到数据的长度,否则将返回-1,若返回值为0,表示连接已经终止,应用程序可以根据返回的值进行不一样的操作。

recv()函数包含一个flags 参数,我们暂时可以直接忽略它,设置为0 即可。注意,如果接收的数据大于用户提供的缓存区,那么多余的数据会被直接丢弃.

3.7 sendto()

函数主要是用于UDP 协议传输数据中,它向另一端的UDP 主机发送一个UDP 报文,本质上是对netconn_send()函数的封装,参数data 指定了要发送数据的起始地址,而size 则指定数据的长度,参数flag 指定了发送时候的一些处理,比如外带数据等,此时我们不需要理会它,一般设置为0 即可,参数to 是一个指向sockaddr 结构体的指针,在这里需要我们自己提供远端主机的IP 地址与端口号,并且用tolen 参数指定这些信息的长度

/** @ingroup socket */
#define sendto(s,dataptr,size,flags,to,tolen)     lwip_sendto(s,dataptr,size,flags,to,tolen)ssize_t
lwip_sendto(int s, const void *data, size_t size, int flags,const struct sockaddr *to, socklen_t tolen);

3.8 send()

send()函数可以用于UDP 协议和TCP 连接发送数据。在调用send()函数之前,必须使用connect()函数将远端主机的IP 地址、端口号与Socket 连接结构进行绑定。对于UDP 协议,send()函数将调用lwip_sendto()函数发送数据,而对于TCP 协议,将调用netconn_write_partly()函数发送数据。相对于sendto()函数,参数基本是没啥区别的,但无需我们设置远端主机的信息,更加方便操作,因此这个函数在实际中使用也是很多的

/** @ingroup socket */
#define send(s,dataptr,size,flags)                lwip_send(s,dataptr,size,flags)ssize_t
lwip_send(int s, const void *data, size_t size, int flags);

3.9 write()

这个函数一般用于处于稳定的TCP 连接中传输数据,当然也能用于UDP 协议中,它也是基于lwip_send 上实现的,但是无需我们设置flag 参数

/** @ingroup socket */
#define write(s,dataptr,len)                      lwip_write(s,dataptr,len)ssize_t
lwip_write(int s, const void *data, size_t size)
{return lwip_send(s, data, size, 0);
}

3.10 close()

close()函数是用于关闭一个指定的套接字,在关闭套接字后,将无法使用对应的套接字描述符索引到连接结构,该函数的本质是对netconn_delete()函数的封装(真正处理的函数是netconn_prepare_delete()),如果连接是TCP 协议,将产生一个请求终止连接的报文发送到对端主机中,如果是UDP 协议,将直接释放UDP 控制块的内容

/** @ingroup socket */
#define close(s)                                  lwip_close(s)int
lwip_close(int s);

3.11 ioctl()、ioctlsocket()

两个函数,其实是一样的,本质是宏定义,都是调用lwip_ioctl()函数,它用于获取与设置套接字相关的操作参数.

s:一个标识套接口的描述字。

cmd:对套接口s的操作命令。

argp:指向cmd命令所带参数的指针

参数cmd 指明对套接字的操作命令,在LwIP中只支持FIONREAD 与FIONBIO 命令:

  • FIONREAD 命令确定套接字s 自动读入的数据量,这些数据已经被接收,但应用线程并未读取的,所以可以使用这个函数来获取这些数据的长度,在这个命令状态下,argp 参数指向一个无符号长整型,用于保存函数的返回值(即未读数据的长度)。如果套接字是SOCK_STREAM类型,则FIONREAD 命令会返回recv()函数中所接收的所有数据量,这通常与在套接字接收缓存队列中排队的数据总量相同;而如果套接字是SOCK_DGRAM类型的,则FIONREAD 命令将返回在套接字接收缓存队列中排队的第一个数据包大小。
  • FIONBIO 命令用于允许或禁止套接字的非阻塞模式。在这个命令下,argp 参数指向一个无符号长整型,如果该值为0 则表示禁止非阻塞模式,而如果该值非0 则表示允许非阻塞模式则。当创建一个套接字的时候,它就处于阻塞模式,也就是
    说非阻塞模式被禁止,这种情况下所有的发送、接收函数都会是阻塞的,直至发送、接收成功才得以继续运行;而如果是非阻塞模式下,所有的发送、接收函数都是不阻塞的,如果发送不出去或者接收不到数据,将直接返回错误代码给用户,这就需要用户对这些“意外”情况进行处理,保证代码的健壮性,这与BSD Socket 是一致的。
/** @ingroup socket */
#define ioctlsocket(s,cmd,argp)                   lwip_ioctl(s,cmd,argp)/** @ingroup socket */
#define ioctl(s,cmd,argp)                         lwip_ioctl(s,cmd,argp)int
lwip_ioctl(int s, long cmd, void *argp);

3.12 setsockopt()

/** @ingroup socket */
#define setsockopt(s,level,optname,opval,optlen) lwip_setsockopt(s,level,optname,opval,optlen)int
lwip_setsockopt(int s, int level, int optname, const void *optval, socklen_t optlen);

这个函数是用于设置套接字的一些选项的,参数level 有多个常见的选项,如:

SOL_SOCKET:表示在Socket 层。

IPPROTO_TCP:表示在TCP 层。

IPPROTO_IP: 表示在IP 层。

参数optname 表示该层的具体选项名称,比如:

  • 1. 对于SOL_SOCKET 选项,可以是SO_REUSEADDR(允许重用本地地址和端口)、SO_SNDTIMEO(设置发送数据超时时间)、SO_SNDTIMEO(设置接收数据超时时间)、SO_RCVBUF(设置发送数据缓冲区大小)等等。
  • 2. 对于IPPROTO_TCP 选项,可以是TCP_NODELAY(不使用Nagle 算法)、TCP_KEEPALIVE(设置TCP 保活时间)等等。
  • 3. 对于IPPROTO_IP 选项,可以是IP_TTL(设置生存时间)、IP_TOS(设置服务类型)等等。

3.13 getsockopt()

这个函数与setsockopt()函数的选项参数及名称都是差不多的,只不过是作用是获得这些选项信息在这里就不过多讲解

4. 实验例程

4.1  TCP Server

#define PORT              5001
#define RECV_DATA         (1024)static void
tcpecho_thread(void *arg)
{int sock = -1,connected;char *recv_data;struct sockaddr_in server_addr,client_addr;socklen_t sin_size;int recv_data_len;recv_data = (char *)pvPortMalloc(RECV_DATA);if (recv_data == NULL){printf("No memory\n");goto __exit;}sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){printf("Socket error\n");goto __exit;}server_addr.sin_family = AF_INET;server_addr.sin_addr.s_addr = INADDR_ANY;server_addr.sin_port = htons(PORT);memset(&(server_addr.sin_zero), 0, sizeof(server_addr.sin_zero));if (bind(sock, (struct sockaddr *)&server_addr, sizeof(struct sockaddr)) == -1){printf("Unable to bind\n");goto __exit;}if (listen(sock, 5) == -1){printf("Listen error\n");goto __exit;}while(1){sin_size = sizeof(struct sockaddr_in);connected = accept(sock, (struct sockaddr *)&client_addr, &sin_size);printf("new client connected from (%s, %d)\n",inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));{int flag = 1;setsockopt(connected,IPPROTO_TCP,     /* set option at TCP level */TCP_NODELAY,     /* name of option */(void *) &flag,  /* the cast is historical cruft */sizeof(int));    /* length of option value */}while(1){recv_data_len = recv(connected, recv_data, RECV_DATA, 0);if (recv_data_len <= 0) break;printf("recv %d len data\n",recv_data_len);write(connected,recv_data,recv_data_len);}if (connected >= 0) closesocket(connected);connected = -1;}
__exit:if (sock >= 0) closesocket(sock);if (recv_data) free(recv_data);
}

4.2  TCP Client

#define PORT              5001
#define IP_ADDR        "192.168.0.100"static void client(void *thread_param)
{int sock = -1;struct sockaddr_in client_addr;uint8_t send_buf[]= "This is a TCP Client test...\n";while(1){sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){printf("Socket error\n");vTaskDelay(10);continue;} client_addr.sin_family = AF_INET;      client_addr.sin_port = htons(PORT);   client_addr.sin_addr.s_addr = inet_addr(IP_ADDR);memset(&(client_addr.sin_zero), 0, sizeof(client_addr.sin_zero));    if (connect(sock, (struct sockaddr *)&client_addr, sizeof(struct sockaddr)) == -1) {printf("Connect failed!\n");closesocket(sock);vTaskDelay(10);continue;}                                           printf("Connect to iperf server successful!\n");while (1){if(write(sock,send_buf,sizeof(send_buf)) < 0)break;vTaskDelay(1000);}closesocket(sock);}}

注意: PC与单板连接的时候,网络调试助手,必须保证防火墙是允许通信的,否则可能被拦截而造成失败。

lwIP TCP/IP 协议栈笔记之十八: Socket接口编程相关推荐

  1. lwIP TCP/IP 协议栈笔记之十五: TCP协议

    目录 1. TCP 服务简介 2. TCP 的特性 2.1 连接机制 2.2 确认与重传 2.3 缓冲机制 2.4 全双工通信 2.5 流量控制 2.6 差错控制 2.7 拥塞控制 3. 端口号的概念 ...

  2. lwIP TCP/IP 协议栈笔记之十九: JPerf 工具测试网速

    目录 1. iPerf 与JPerf 2. 测试网络速度 2.1 获取JPerf 网络测速工具 2.2 测试开发板接收速度(NETCONN API) 2.3 测试开发板接收速度(Socket API) ...

  3. stm32 网络 服务器通信协议,利用stm32的lwip TCP/IP协议栈的通信的思路

    利用stm32f103vet6作为平台,enc28j60网卡,lwip tcp/ip作为协议栈进行相应的程序编写. Stm32作为服务器与stm32作为客户端程序编写的基本步骤,思路清理: 1.stm ...

  4. RT-Thread 之 WIZnet 软件包(全硬件TCP/IP协议栈W5500以太网芯片) 以太网 Socket 通信

    目录 1.介绍 1.1 WIZnet 1.2 W5500 2.RT-Thread Studio 配置及相关代码 2.1 添加WIZnet软件包 2.2 配置 WIZnet 软件包 2.3 配置 spi ...

  5. Linux TCP/IP协议栈笔记

    数据包的接收 作者:kendo Kernel:2.6.12 一.从网卡说起 这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析.这里以Intel的e100驱动为例进行分析 ...

  6. 2022Java学习笔记八十八(网络编程:UDP通信,一发一收,多发多收消息接收实现)

    2022Java学习笔记七十八(网络编程:UDP通信,一发一收,多发多收消息接收实现) 一.快速入门 DatagramPacket:数据包对象 实例代码 定义发送端 package com.zcl.d ...

  7. STM32F103驱动SDIO wifi Marvell8801/Marvell88w8801 介绍(十) ---- 移植TCP/IP协议栈LWIP

    代码工程的GITHUB连接:点进进入GITHUB仓库 https://github.com/sj15712795029/stm32f1_marvell88w8801_marvell8801_wifi ...

  8. TCP/IP协议栈Lwip的设计与实现:之一

    目录 摘要: 1.介绍 2.协议分层 3.综述 4.进程模型 5.操作系统仿真层 6.缓冲与存储管理 6.1包缓冲----pbufs 6.2内存管理 摘要: LWIP是TCP/IP协议栈的实现.LWI ...

  9. TCP/IP协议栈Lwip的设计与实现:之三

    接上文:TCP/IP协议栈Lwip的设计与实现:之二_龙赤子的博客-CSDN博客 目录 10.TCP处理 10.1概述 10.2数据结构 10.3序列号计算 10.4数据入队和传输 10.5接收段数据 ...

最新文章

  1. 十九、Redis 6.0 的客户端缓存
  2. Ubuntu 下安装adobe flash player
  3. close关闭指定窗口 matlab_Δ-Σ ADC设计笔记一:MATLAB环境设置
  4. 【TensorFlow-windows】学习笔记四——模型构建、保存与使用
  5. P5112 FZOUTSY
  6. 一次性输血器行业调研报告 - 市场现状分析与发展前景预测(2021-2027年)
  7. 团队作业—第二阶段08
  8. Python深入理解yield
  9. 一款给变量自动取名的工具
  10. 本地数据库数据导入linux
  11. 微信指数:微信关键词搜索热度情况分析!
  12. c#、cefsharp 获取、提取 img、image 图片标签二进制数据
  13. 关于博客的书写——读刘末鹏博客学习方法篇有感
  14. Groovy脚本基础全攻略
  15. winrar正确破解方法
  16. 【机器学习入门——1】Python 开发环境的安装 Python(x,y)及Pycharm
  17. java保证一段代码枷锁_Java堆外内存之突破JVM枷锁
  18. 8090该如何创业呢?
  19. 关于maven-jar-plugin报红在IDEA中配置Maven时,总是遇到org.apache.maven.plugins:maven-clean-plugin:2.4这样报错。而且一报就是全红
  20. 【放置江湖】LUA手游 基于HOOK 解密修改流程

热门文章

  1. Redis 管理工具:Another Redis DeskTop Manager
  2. C语言——逆序输出字符串的函数实现
  3. 云原生分布式应用性能监控实践-天眼全流程调用链
  4. 市场定位(Marketing Positioning)
  5. 兴趣社交再度站在风口,谁能成功入局?
  6. 自动驾驶的基本过程(三):线控
  7. 澜讯 | 时尚集团携手数澜科技 引领传统内容数字化新风潮
  8. xmanager xstart连接linux桌面,如何使用xstart运行CDE,KDE以及Gnome?
  9. SpringCloud (十一) --------- Stream 消息驱动框架
  10. Docker 来点好玩的