TCP聊天室(一):tcp通讯

前言

在学习完Go语言之后,总是感觉没有合适的上手项目进行练习,最近正好看到一个TCP网络聊天室的小项目,这个项目只使用基础的包而不使用任何框架,非常适合练手。

需要的工具有

  • Go开发环境
  • nc工具,方便模拟client进行测试

建立连接

Go中,我们可以使用net包来进行基本的serversocket的创建,也就是net.Listen方法

1
net.Listen()

下面是这个函数的原型

1
2
3
4
5
// The network must be "tcp", "tcp4", "tcp6", "unix" or "unixpacket".
func Listen(network, address string) (Listener, error) {
	var lc ListenConfig
	return lc.Listen(context.Background(), network, address)
}

可以看到,函数的两个参数都是string类型的,第一个参数指定的是通信的网络(可以直接指定tcp), 第二个是server的地址。返回值就是监听对象和err

例如,我们想要启动一个监听,就可以这样写

1
2
3
4
listener, err := net.Listen("tcp", "127.0.0.1:8080")
if err != nil {
    // To do
}

就创建好了一个scoket

在得到listener后,要开启接受功能,可以调用

1
listener.Accept()

同样,这个函数有两个返回值,正常使用是这样的

1
2
3
4
conn, err := listener.Accept()
if err != nil {
    // To do
}

其中,返回的是一个net.Conn类型的参数,可以通过connsocket之间传递数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type Conn interface {
	// Read reads data from the connection.
	// Read can be made to time out and return an error after a fixed
	// time limit; see SetDeadline and SetReadDeadline.
	Read(b []byte) (n int, err error)

	// Write writes data to the connection.
	// Write can be made to time out and return an error after a fixed
	// time limit; see SetDeadline and SetWriteDeadline.
	Write(b []byte) (n int, err error)

	// Close closes the connection.
	// Any blocked Read or Write operations will be unblocked and return errors.
	Close() error

	// LocalAddr returns the local network address, if known.
	LocalAddr() Addr

	// RemoteAddr returns the remote network address, if known.
	RemoteAddr() Addr
    
    // ........
    // ........
}

conn.Read可以从连接中读取数据(server可以read来自client的数据), 同时conn.Write可以从连接中发送数据(serverclient发送数据)

这里就实现了通信的基石,即发送和接受数据,其整个过程就是

  • 创建socketnet.Listen,返回一个net.Listener对象
  • 开始接收请求:listener.Accept,返回一个net.Conn对象
  • 使用net.Conn实现接收数据和发送数据

simple demo

这里创建一个简单的demo程序,在server接收到来自client的数据后,把接受到的数据全部转换为大写后发送给client

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"bytes"
	"fmt"
	"net"
)

func main() {
	ip := "127.0.0.1"
	port := 8080
	//CreateServer(ip, port)
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port))
	defer listener.Close()
	if err != nil {
		fmt.Println("Eror!", err)
		return
	}
	buf := make([]byte, 1024)
	conn, err := listener.Accept()
	defer conn.Close()
	for {
		if err != nil {
			fmt.Println("connect fail", err)
			return
		}
        // 读取数据
		conn.Read(buf)
        // 写回数据
		conn.Write(bytes.ToUpper(buf))
	}
}

然后使用nc工具

1
nc 127.0.0.1 8080

当我们发送hello的时候,server正确的返回了HELLO

思考:当有多个client的时候怎么办?

协程处理

只有一个用户创建连接的时候可以正常返回,但此时有多个用户创建了连接请求,由于我们只accept了一次连接请求,所以当多个用户尝试连接的时候,第二个及之后的那些用户无法与服务器建立连接。

解决办法

  • 每次在循环的过程中不断的进行监听,而不是只监听一次。 原始代码是

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    // .....
    conn, err := listener.Accept() // 把这里添加到循环中
    	defer conn.Close()
    	for {
    		if err != nil {
    			fmt.Println("connect fail", err)
    			return
    		}
            // 读取数据
    		conn.Read(buf)
            // 写回数据
    		conn.Write(bytes.ToUpper(buf))
    	}
    }
    

    添加到循环后就可以不断的建立连接

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    // ......
    
    	for {
            conn, err := listener.Accept()
    		if err != nil {
    			fmt.Println("connect fail", err)
    			return
    		}
            // 读取数据
    		conn.Read(buf)
            // 写回数据
    		conn.Write(bytes.ToUpper(buf))
    	}
    }
    

    然后再新建client的时候就可以处理多用户连接。

    这样写有什么问题?

    可以发现,当在一个client发送第二组数据后,server什么都没有返回,这是因为在循环执行到

    1
    
    conn.Write(bytes.ToUpper(buf))
    

    server一直在期待新的链接,而不是去处理之前的client的数据

  • 使用go协程

    在每次conn成功后,为了保持后续的链接,可以把后续的readwrite封装为go协程

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    
    package main
    
    import (
    	"bytes"
    	"fmt"
    	"net"
    )
    
    func Handler(conn net.Conn) {
    	defer conn.Close()
    	buf := make([]byte, 1024)
    	for {
    		cnt, _ := conn.Read(buf)
    		conn.Write(bytes.ToUpper(buf[:cnt]))
    	}
    }
    
    func main() {
    	ip := "127.0.0.1"
    	port := 8080
    	//CreateServer(ip, port)
    	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port))
    	defer listener.Close()
    	if err != nil {
    		fmt.Println("Eror!", err)
    		return
    	}
    	//defer conn.Close()
    	for {
    		conn, err := listener.Accept()
    		if err != nil {
    			fmt.Println("connect fail", err)
    			return
    		}
    		go Handler(conn)
    	}
    }
    

    也就是说在主函数内,只负责去监听是否有用户链接,而链接后的读写就去创建一个新的协程,在这个协程内根据这个链接不断的去实现client-server之间的读写。

总结

  • net.Listen:创建tcp socket, 返回listener对象
  • listener.Accept:监听客户端的连接, 返回net.Conn连接对象
  • net.Conn:实现readwrite,读取和发送数据
  • go:开启一个协程
Licensed under CC BY-NC-SA 4.0
花有重开日,人无再少年
使用 Hugo 构建
主题 StackJimmy 设计