Skip to content
This repository has been archived by the owner on Apr 3, 2021. It is now read-only.

关于tcp流量的阻塞问题 #100

Closed
raintean opened this issue Feb 28, 2020 · 23 comments
Closed

关于tcp流量的阻塞问题 #100

raintean opened this issue Feb 28, 2020 · 23 comments
Labels

Comments

@raintean
Copy link

目前发现一个问题:
在有TCP数据从tun方向过来的时候. 会进到Receive方法

func (conn *tcpConn) Receive(data []byte) error {
	if err := conn.receiveCheck(); err != nil {
		return err
	}
	n, err := conn.sndPipeWriter.Write(data)
	if err != nil {
		return NewLWIPError(LWIP_ERR_CLSD)
	}
	C.tcp_recved(conn.pcb, C.u16_t(n))
	return NewLWIPError(LWIP_ERR_OK)
}

然后再往pipe中写入, 等待对端Read, 然后使用自己的逻辑(直连/Socks等)发送出去.
但是这里存在一个问题. 如果没法及时调用Read方法(比如转发速度太慢等), 会造成Receive方法长时间阻塞在 conn.sndPipeWriter.Write(data). 由于lwip是单线程, 所以其对于Receive的调用迟迟无法返回. 造成其他的网络流量, 全部阻塞.
目前作者有没有什么好的方案来解决该问题.
个人认为这就是一个流的背压传递问题.

@fbion
Copy link

fbion commented Feb 28, 2020

https://github.com/FlowerWrong/tun2socks
这个有这个问题吗

@raintean
Copy link
Author

https://github.com/FlowerWrong/tun2socks
这个有这个问题吗

这个应该没有, 我记得他是基于google的netstack实现的. 不过我更看好作者的这个实现, 轻量, 内存可控性好.

@eycorsican
Copy link
Owner

如果暂时没法处理数据,比如连接的发送缓存满了,可以给 lwip 返回一个错误

case LWIP_ERR_CONN:

但 go 现在的连接是同步写,所以还要在 handler 里实现多一级缓存,考虑实际上连接的写操作很少会有阻塞,我觉得没什么必要。

@raintean
Copy link
Author

raintean commented Feb 29, 2020

在使用过程中确实会出现这样的情况, 比如上传走的是代理路线, 如果代理服务器不稳定. 那么按这个情况来说, 是会阻塞直连, 或者其他代理路线的上传(包括下载).
而且在网络环境较差的情况下(这个项目用来做啥,大家众所周知,所以网络环境....)特别明显.

目前我的解决方案是:

type tcpConn struct {
	sync.Mutex

	pcb           *C.struct_tcp_pcb
	handler       TCPConnHandler
	remoteAddr    *net.TCPAddr
	localAddr     *net.TCPAddr
	connKeyArg    unsafe.Pointer
	connKey       uint32
	canWrite      *sync.Cond // Condition variable to implement TCP backpressure.
	state         tcpConnState
	sndPipeReader *io.PipeReader    //换成带缓存的实现
	sndPipeWriter *io.PipeWriter    //换成带缓存的实现
	closeOnce     sync.Once
	closeErr      error
}

将pipe出来的 sndPipeReader, sndPipeWriter 更换成带缓存的实现.
其次将

func (conn *tcpConn) Receive(data []byte) error {
	if err := conn.receiveCheck(); err != nil {
		return err
	}
	n, err := conn.sndPipeWriter.Write(data)
	if err != nil {
		return NewLWIPError(LWIP_ERR_CLSD)
	}
	C.tcp_recved(conn.pcb, C.u16_t(n))    //删除这行
	return NewLWIPError(LWIP_ERR_OK)
}

中的 C.tcp_recved(conn.pcb, C.u16_t(n)) 删掉. 因为就算无阻塞写入了缓存pipe中, 也并不意味着数据已经接收. 如果这个时候tcp_recved 那么lwip还是会疯狂发数据, 造成缓存疯狂增长(在来不及Read的情况下).
最后在Read方法中:

func (conn *tcpConn) Read(data []byte) (int, error) {
         // xxxxxxxx

	lwipMutex.Lock()
	C.tcp_recved(conn.pcb, C.u16_t(n))   //在真正read的时候,告诉lwip,数据已经接收
	lwipMutex.Unlock()
	return n, err
}

调用 tcp_recved, 表示数据已经进入到转发那边去了, 让转发的背压, 能够很好的传递给 lwip 层.

个人认为, 这就是一个因为cgo的实现, 造成的阻塞世界和非阻塞世界思想不匹配的问题.

另外我觉得在应用的层面去实现多一级缓存, 并不清真, 让本身库的职责外泄了. 不优雅.

@eycorsican
Copy link
Owner

把缓存放在 core 里也是可以的,但缓存不可能设到无限大,满了后依然要返回错误给 lwip

@raintean
Copy link
Author

把缓存放在 core 里也是可以的,但缓存不可能设到无限大,满了后依然要返回错误给 lwip

这不会有缓存扩大的问题. 写入时只是为了不阻塞, 放入缓存, 如果不去调用tcp_recved来更新可用的发送窗口, 实际上, lwip不会再次传入数据了. 除非你在read的时候调用tcp_recved让窗口更新. 所以不需要判断缓存满不满的情况, 因为缓存最大也就是lwip的发送窗口大小. 这边缓存到达最大. 那边就是0了. lwip不会再发数据过来了.

@eycorsican
Copy link
Owner

那你实现一下?

@raintean
Copy link
Author

raintean commented Feb 29, 2020

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif

@wongsyrone
Copy link

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif

能把改过的代码库推上来吗

@raintean
Copy link
Author

raintean commented Mar 1, 2020

那你实现一下?

我实现了, 但是有点问题. 偶尔会出现lwip(tcp.c)中的new_rcv_ann_wnd错误

#if !LWIP_WND_SCALE
      LWIP_ASSERT("new_rcv_ann_wnd <= 0xffff", new_rcv_ann_wnd <= 0xffff);
#endif

能把改过的代码库推上来吗

@eycorsican @wongsyrone 代码已经上来

@wongsyrone
Copy link

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

@raintean
Copy link
Author

raintean commented Mar 3, 2020

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

根据lwip的文档中tcp_recved的说明, 以及tcp协议关于发送窗口的规定. 是没有问题.

@wongsyrone
Copy link

有个疑问,如果目前代码只把tcp_recved挪到Read里面能不能解决问题呢

根据lwip的文档中tcp_recved的说明, 以及tcp协议关于发送窗口的规定. 是没有问题.

不过目前repo中使用的lwip配置,貌似窗口都是固定大小

@raintean
Copy link
Author

raintean commented Mar 3, 2020

这里只是和wnd的scale无关, 影响的是可用的wnd

@Fndroid
Copy link

Fndroid commented Apr 21, 2020

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

@raintean
Copy link
Author

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

没太明白你的意思, 可否讲的详细一点

@Fndroid
Copy link

Fndroid commented Apr 21, 2020

@raintean @eycorsican 用core的时候遇到这个问题了,Handle过来的net.Conn似乎只能等读写完成才能下一个,这样下游调用的话做不了缓存吧

没太明白你的意思, 可否讲的详细一点

情况是这样的,我这里使用go-tun2socks/core的时候,实现了一个core.TCPConnnHandler,里面的Handler会把TAP收到的net.Conn传过来,但是这个时候我还需要去Dial远端服务器才能relay,设置了5s的超时,这个时候这个要是一个请求阻塞了,后面的就都得等5s,不知道有没有办法解决

@raintean
Copy link
Author

raintean commented Apr 21, 2020

@Fndroid 你这个问题和该issue没关系, 不过我可以回答一下你. 其实作者在代码里面也写了. 不要让handle被阻塞, 那么问题很明显了. 你handle方法里面 直接开一个协程去处理过来的net.Conn, 让handle能够快速返回 不就OK了嘛.

@Fndroid
Copy link

Fndroid commented Apr 21, 2020

@raintean 感谢回复。我刚认真测试了一下,我发现的现象是Handle传过来的Conn在Read(copyBuffer)的时候要等很长一段时间,请问这个是什么原因造成的呢?也试过拿到Conn的时候直接Read,但是也需要很长的时间才能EOF

@raintean
Copy link
Author

@raintean 感谢回复。我刚认真测试了一下,我发现的现象是Handle传过来的Conn在Read(copyBuffer)的时候要等很长一段时间,请问这个是什么原因造成的呢?也试过拿到Conn的时候直接Read,但是也需要很长的时间才能EOF

可能就是没有数据能read呢? 对不对...

@Fndroid
Copy link

Fndroid commented Apr 22, 2020

@raintean 最后问一下大佬,这个duplexConn的意义是什么呢?Relay的时候Dst Conn是不是也需要实现这个接口呢,谢谢

type duplexConn interface {
net.Conn
CloseRead() error
CloseWrite() error
}

@raintean
Copy link
Author

@Fndroid 具体不清楚, 字面上的意思是全双工连接. 看接口你能明白, 他是可以单独关闭读取或者写入的, net.Conn的话, 好像是close就全部关闭了. 这个在reply的时候有意义. 比如 A <-> B 直接做pipe, A已经EOF了, 这个时候其实是可以关闭B的写入的. 大概就是这么一个意思. 你不太需要关心这个. 直接net.Conn也能满足了. 这个只是对上下行都做了分开控制.

@github-actions
Copy link

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants