模型

了解socket的模型也是非常重要的,可以帮助我们对socket编程有一个更宏观的了解。从tcp socket诞生后,网络编程架构也经过几轮演化,大致流程如下:

  1. 每个进程一个连接
  2. 每个线程一个连接
  3. Non block + I/O对路复用

伴随着模型的演化,服务器程序愈加强大,可以支持更多的连接,获得更好的性能处理。目前主流web server一般均采用的是Non-Block + I/O多路复用,有的也结合了多线程,多进程。不过I/O复用也给使用者带来不小的复杂度,以至于后续出现了许多高性能的I/O多路复用框架,以帮助开发者简化开发的复杂度,降低心智负担。不过Go的设计者似乎认为I/O多路复用的这种通过回调机制控制流的方式依旧复杂,并且有悖于“一般逻辑”设计,为此Go语言将改“复杂性”隐藏在runtime中。Go开发者无需关注socket是否是non-block的,也无需亲自注册文件描述符的回调,只需在每个连接对应的goroutine中以“block I/O”的方式对待socket处理即可,这可以说是大大降低了开发人员的心智负担,一个典型的Go server端程序大致如下:

  1. type Conn struct {
  2. rwc io.ReadWriteCloser
  3. br *bufio.Reader
  4. buffer []byte
  5. }
  6. func NewConn(rwc io.ReadWriteCloser) *Conn {
  7. return &Conn{
  8. rwc: rwc,
  9. br: bufio.NewReader(rwc),
  10. buffer: make([]byte, messageBufferLen),
  11. }
  12. }
  13. func main(){
  14. l, err: = net.Listen("tcp", ":8080")
  15. if err != nil {
  16. fmt.Println("listen error:", err)
  17. return
  18. }
  19. for {
  20. c, err := l.Accept()
  21. if err != nil {
  22. fmt.Println("accept error:", err)
  23. return
  24. }
  25. conn := duss.NewConn(c)
  26. go handler.Run()
  27. }
  28. }

用户空间中看到的goroutine的block socket, 实际上是通过Go runtime中的netpoller通过Non-block socket + I/O多路复用机制模拟出来的,真实的underlying socket实际上是non-block的,只是runtime拦截了底层socket系统调用的错误码,并通过netpoller和goroutine调度让goroutine阻塞在用户层得到的socket fd上。比如,当用户层针对某个socket fd发起read操作时,如果该socket fd中暂时没有数据,那么runtime会将socket id加入到netpoller中监听,同时对应的goroutine挂起,知道runtime收到socket fd数据ready通知,runtime才会重新唤醒等待在该socket fd上准备read的那个Goroutine。而这个过程从Gotouine的角度来看,就像是read一直block在那个socket fd上似的。

conn read的行为特点

socket中无数据

连接建立之后,如果对方未发送数据到socket,接收方server会阻塞在read操作上,这和前面提到的“模型”原理是一致的。执行该read操作的goroutine也会被挂起,runtime会监视该socket,直到其有数据才会重新调度该socket对应的goroutine完成read。

socket中有部分数据

如果socket中部分数据,而且长度小于一次read操作所期望读出的数据长度,那么read将会成功读出这部分数据并成功返回,而不是等待所有期望数据全部读取后再返回。

  1. func handlerConn(c net.Conn) {
  2. defer c.Close()
  3. for {
  4. var buf = make([]byte, 10)
  5. log.Println("start to read from conn")
  6. n, err := c.Read(buf)
  7. if err != nil {
  8. log.Println("conn read error:", err)
  9. return
  10. }
  11. log.Printf("read %d bytes, contents is %s\n", n, string(buf[:n]))
  12. }
  13. }
  14. //read 2 bytes, content is hi

socket中有足够的数据

如果socket中有数据,而且长度大于等于一次read操作所期望读出的长度,而且长度大于一次read操作所期望读出的数据长度,那么read将会成功读出这部分数据并返回。这个场景最符合我们对read的期待了,read将用socket中的数据将我们传入slice填满后返回,n=10, err != nil。

我们通过client2.go向server发送如下内容,abcdefghij12345,执行结果如下:

  1. ¥go run server2.go
  2. // accept a new connection
  3. // start to read from conn
  4. // read 10 bytes, content is abcdefghij
  5. // start to read from conn
  6. // read 5 bytes, content is 12345

client端发送的内容长度为15个字节,server端read buffer的长度为10,因此serve read第一次返回时只会读取10个字节,socket中还剩下5个字节数据,server再次read时会把剩余的数据读出。

conn write的行为特点

写成功

所谓成功写就是write调用返回的n与预期要写入的数据长度相等,而且err = nil, 这是我们在调用write遇到的最常见的情形,这里就不用举例子了。

写阻塞

当服务端调用write后,实际上数据是写入到OS的协议栈的数据缓冲中。TCP是全双工通信,因此每个方向都有独立的数据缓冲,当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后,write就会阻塞。

参考

Go语言TCP Socket编程