注意,本文提供的代码来自本人搞起耍的 netstack,有一些类似 tun2socks LwIP 实现,目前不会考虑集成到产品上面作为可选 TCP/IP 网络栈,当然不会是基于 go-gvisor、go-netstack 栈,C#/C++ 搞的无第三方代码专用 TCP/IP 网络栈。
LwIP协议栈是无法被多线程驱动的,否则可能出现难以处理的内存安全问题,另外与操作系统TCP/IP协议栈提供接口类似,每个 TCP/IP Socket 实例的发送缓冲区大小是固定的,意味着不可以一直向LwIP写入段数据(TCP_SEG)到缓冲区,LwIP上为 TCP_PCB(TCP协议控制块)本质上它就代表着一个确切的TCP实例。
LwIP上面虽然外层接口使用TCP_PCB参数数字签名来做监听控制块,但步入内部会被重分配重叠TCP_PCB_LISTEN结构内存,具体细节自行阅读:tcp_listen_with_backlog_and_err
LwIP 由于需要工作在 STA(单线程)架构环境上面,包括关联的TCP事件回调(如:tcp_recv、tcp_sent)所以为了可以让 LwIP 在单个线程上面处理尽可能大的I/O网络吞吐量,所以不可以让用户增加的代码对单个核心CPU负担很大,网络游戏加速器不需要考虑,类似需要为流做 AES-256-CFB 加密,那可就不行,单个核心CPU算力负担太大,那么好的办法是通过 Asynchronous Socket I/O 把负担偏重的任务交付到另外的 MTA 架构由 Coroutine、EDSM 驱动的模型处理。
本文例子是由 C/C++ 11 语言实现,依赖 boost::asio,不喜欢 boost::asio,可以考虑使用 libevent、libuv,一个 C/C++ 语言库,一个 C 语言库,需要逼格,大家了解!
另外可以把整个 LwIP 一些结构还有代码魔改以下,直接支持 C/C++ std::function、std::shared_ptr 等等,如果不希望过多的魔改 LwIP 协议栈实现代码,那么关联每个 PCB 实力具体的用户结构,本文代码为:“netstack_tcp_socket”,否则可以 tcp_arg(PCB, XXX) or PCB->callback_arg = XXX 来设置 LwIP 协议栈TCP事件触发时传入的 arg 参数值,目前方法有几类,Key 2 Instance 表、放结构的安全/不安全指针上去。
关于:
netstack_tcp_send 网络栈TCP发送数据
inline static err_t
netstack_tcp_send(struct tcp_pcb* pcb, void* data, u16_t len, const std::function<void(struct tcp_pcb*)>& callback) {
err_t err = ERR_ARG;
if (pcb) {
std::shared_ptr<netstack_tcp_socket> socket_ = netstack_tcp_getsocket(pcb->callback_arg);
if (!socket_) {
err = ERR_ABRT;
goto ret_;
}
err = ERR_OK;
if (data && len) {
LinkedList<netstack_tcp_socket::send_context>& sents = socket_->sents[netstack_tcp_socket::ENETSTACK_TCP_SENT_LWIP];
if (() > 0) {
queue_:
char* chunk_ = (char*)malloc(len);
memcpy(chunk_, data, len);
LinkedListNode<netstack_tcp_socket::send_context>* node_ = new LinkedListNode<netstack_tcp_socket::send_context>();
netstack_tcp_socket::send_context& context_ = node_->Value;
context_.buf = { chunk_, len };
context_.cb = callback;
(node_);
goto ret_;
}
err = tcp_write(pcb, data, len, TCP_WRITE_FLAG_COPY);
if (err == ERR_OK) {
if (callback) {
callback(pcb);
}
goto ret_;
}
if (err == ERR_MEM) {
err = ERR_OK;
goto queue_;
}
}
ret_:
tcp_output(pcb);
}
return err;
}
朋友们或许可能会疑惑为什么,上述代码只调用 tcp_write 向 LwIP 协议栈写入欲发送的TCP段数据,而不提前使用 tcp_sndbuf 宏获取TCP_PCB上面的当前可用发送滑块窗口大小,来决定当前可立即传送多少字节。
那么我的答案是:没有意义,tcp_write 函数内部实现已经处理发送缓冲区不足或内存分配失败的情况会返回 ERR_MEM,不工作在超低配的单片机设备上面,ERR_MEM 返回只有一个可能是发送缓冲区大小不足。
netstack_tcp_dosent TCP已传送字节数回调函数,需要为每个TCP_PCB控制块,使用 tcp_sent(pcb, netstack_tcp_dosent ); 进行回调绑定,不喜欢调用函数,可以直接设置TCP_PCB结构上面的 sent 字段。
inline static err_t
netstack_tcp_dosent(void* arg, struct tcp_pcb* pcb, u16_t len) {
if (pcb) {
std::shared_ptr<netstack_tcp_socket> socket_ = netstack_tcp_getsocket(pcb->callback_arg);
if (socket_) {
LinkedList<netstack_tcp_socket::send_context>& sents = socket_->sents[netstack_tcp_socket::ENETSTACK_TCP_SENT_LWIP];
while (() > 0) { // tcp_sndbuf
LinkedListNode<netstack_tcp_socket::send_context>* node = ();
netstack_tcp_socket::send_context context_ = node->Value;
char* unseg_ = (char*)context_.;
int unsent_ = context_.;
err_t err_ = tcp_write(pcb, unseg_, unsent_, TCP_WRITE_FLAG_COPY);
if (err_ != ERR_OK) {
break;
}
else {
free(unseg_);
();
delete node;
}
if (context_.cb) {
context_.cb(pcb);
}
}
}
tcp_output(pcb);
}
return ERR_OK;
}
关于 netstack_tcp_socket 结构的定义,其实把你不一定需要这么多字段,具体看您的代码实现,本文仅提供正确使用 LwIP 网络协议栈发送TCP/IP协议数据,并确保传送流字节顺序一致性。
typedef struct {
typedef struct {
void* p;
int sz;
} buffer_chunk;
typedef struct {
buffer_chunk buf;
std::function<void(struct tcp_pcb*)> cb;
} send_context;
typedef enum {
ENETSTACK_TCP_SENT_LWIP,
ENETSTACK_TCP_SENT_SOCK,
ENETSTACK_TCP_SENT_MAX
} ENETSTACK_TCP_SENT_BUFS;
LinkedList<send_context> sents[ENETSTACK_TCP_SENT_MAX];
std::shared_ptr<boost::asio::ip::tcp::socket> socket;
bool open;
struct tcp_pcb* pcb;
ip_addr_t local_ip;
u16_t local_port;
ip_addr_t remote_ip;
u16_t remote_port;
u8_t buf[16384];
} netstack_tcp_socket;