echo server

在一个服务器上注册并监听一个端口。然后它阻塞在一个 Accept 操作,并等待客户端连接。当一个客户端连接, Accept调用返回一个连接 Connection 对象

func main() {
	var err error
	var ln net.Listener
	ln, err = net.Listen("tcp", ":9999")
	defer ln.Close()

	for {
		conn, err := ln.Accept()
		go handleRequest(conn)
	}
}

func handleRequest(conn net.Conn) {
	defer conn.Close()
	for {
		io.Copy(conn, conn)
	}
}

Accept

  1. 首先直接尝试通过 socket 执行 accept 来获取可能的客户端连接
  2. 如果此时客户端没有连接,因为 socket 是非阻塞模式,会直接返回 EAGAIN
  3. 调用 runtime.poll_runtime_pollWait 将当前协程挂起,并且根据是等待读还是等待写将当前 g 的引用保存到 pollDesc 中的 rg 或者 wg
  4. 当有新的客户端连接到来时,epoll 会通知将当前阻塞的协程恢复,然后重新执行第一步

Go 使用非阻塞IO来防止大量系统线程阻塞带来的上下文切换,取而代之的是让轻量级的协程阻塞在 IO 事件上,然后通过epoll 来实现IO事件通知,唤醒阻塞的协程。

echo client

一旦客户端已经建立TCP服务, 就可以"拨号"了. 如果成功,该调用返回一个用于通信的TCPConn。客户端和服务器通过它交换消息。通常情况下,客户端使用TCPConn写入请求到服务器, 并从TCPConn的读取响应。持续如此,直到任一(或两者)的两侧关闭连接。客户端使用该函数建立一个TCP连接

同步机制

Share memory by communicating, don't communicate by sharing memory

因为客户端发送和接收是在两个goroutine 中,main 函数中如果不加上同步机制, 客户端还没有发送接收就执行完了

通过channel实现同步,读和写完成后分别往channel中写入"done",main读取channel中的值,当两个done都读取到后就知道读写已经完成。

func main() {
	conn, err := net.Dial("tcp", ":9000")
	defer conn.Close()
	
	done := make(chan string)
	go handleWrite(conn, done)
	go handleRead(conn, done)

	fmt.Println(<-done)
	fmt.Println(<-done)
}

func handleWrite(conn net.Conn, done chan string) {
	for i := 10; i > 0; i-- {
		_, e := conn.Write([]byte("hello " + strconv.Itoa(i) + "\\r\\n"))
	}
	done <- "sent"
}

func handleRead(conn net.Conn, done chan string) {
	buf := make([]byte, 1024)
	reqLen, err := conn.Read(buf)
	fmt.Println(string(buf[:reqLen-1]))
	done <- "read"
}

net.Dial建立连接, handleWrite发送十个请求, handleRead 接收服务器的响应。一旦完成,往 channel 中写 done