根据 用Golang实现 服务器/客户端 将其改造成一个文本信息聊天室
服务端的改动
服务器为了实现聊天信息的群体广播, 需要记录所有连接到服务器的客户端信息, 所以, 我们需要添加一个集合来保存所有客户端的连接:
var ConnMap map[string]*net.TCPConn
接着, 每次当有新的客户端连接到服务器时, 需要把这个客户端连接行信息加入集合:
ConnMap[tcpConn.RemoteAddr().String()] = tcpConn
当服务器收到客户端的聊天信息时, 需要广播到所有客户端, 所以我们需要利用上面保存TCPConn的map来遍历所有TCPConn进行广播, 用以下方法实现:
1 2 3 4 5 6
| func boradcastMessage(message string) { b := []byte(message) for _, conn := range ConnMap { conn.Write(b) } }
|
客户端代码改动
客户端代码改动相对简单, 只是加入了用户自己输入聊天信息的功能, 在连接成功并且启动了消息接收的gorountine后, 加入以下代码:
1 2 3 4 5 6 7 8 9
| for { var msg string fmt.Scanln(&msg) if msg == "quit" { break } b := []byte(msg + "\n") conn.Write(b) }
|
完整的服务端 server.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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| package main
import ( "bufio" "fmt" "net" )
var ConnMap map[string]*net.TCPConn
func main() { var tcpAddr *net.TCPAddr ConnMap = make(map[string]*net.TCPConn) tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
tcpListener, _ := net.ListenTCP("tcp", tcpAddr)
defer tcpListener.Close()
for { tcpConn, err := tcpListener.AcceptTCP() if err != nil { continue }
fmt.Println("A client connected : " + tcpConn.RemoteAddr().String()) ConnMap[tcpConn.RemoteAddr().String()] = tcpConn go tcpPipe(tcpConn) }
}
func tcpPipe(conn *net.TCPConn) { ipStr := conn.RemoteAddr().String() defer func() { fmt.Println("disconnected :" + ipStr) conn.Close() }() reader := bufio.NewReader(conn)
for { message, err := reader.ReadString('\n') if err != nil { return } fmt.Println(conn.RemoteAddr().String() + ":" + string(message)) boradcastMessage(conn.RemoteAddr().String() + ":" + string(message)) } }
func boradcastMessage(message string) { b := []byte(message) for _, conn := range ConnMap { conn.Write(b) } }
|
客户端 client.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 38 39 40 41
| package main
import ( "bufio" "fmt" "net" )
func main() { var tcpAddr *net.TCPAddr tcpAddr, _ = net.ResolveTCPAddr("tcp", "127.0.0.1:9999")
conn, _ := net.DialTCP("tcp", nil, tcpAddr) defer conn.Close() fmt.Println("connected!")
go onMessageRecived(conn)
for { var msg string fmt.Scanln(&msg) if msg == "quit" { break } b := []byte(msg + "\n") conn.Write(b) } }
func onMessageRecived(conn *net.TCPConn) { reader := bufio.NewReader(conn) for { msg, err := reader.ReadString('\n') fmt.Println(msg) if err != nil { panic(err) break } } }
|
最后分别编译
先启动server端, 然后新开两个终端, 启动客户端, 在其中一个客户端里键入聊天信息后回车, 会发现另外一个客户端收到了刚刚发送的聊天信息
服务器的粘包处理
什么是粘包
一个完成的消息可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这个就是TCP的拆包和封包问题
TCP粘包和拆包产生的原因
- 应用程序写入数据的字节大小大于套接字发送缓冲区的大小
- 进行MSS大小的TCP分段。MSS是最大报文段长度的缩写。MSS是TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。所以MSS并不是TCP报文段的最大长度,而是:MSS=TCP报文段长度-TCP首部长度
- 以太网的payload大于MTU进行IP分片。MTU指:一种通信协议的某一层上面所能通过的最大数据包大小。如果IP层有一个数据包要传,而且数据的长度比链路层的MTU大,那么IP层就会进行分片,把数据包分成若干片,让每一片都不超过MTU。注意,IP分片可以发生在原始发送端主机上,也可以发生在中间路由器上
TCP粘包和拆包的解决策略
- 消息定长。例如100字节
- 在包尾部增加回车或者空格符等特殊字符进行分割,典型的如FTP协议
- 将消息分为消息头和消息尾
- 其它复杂的协议,如RTMP协议等
处理方式
- 发送方在每次发送消息时将消息长度写入一个int32作为包头一并发送出去, 我们称之为Encode
- 接受方则先读取一个int32的长度的消息长度信息, 再根据长度读取相应长的byte数据, 称之为Decode
在实验环境中的主文件夹内, 新建一个名为codec的文件夹在其之下新建一个文件codec.go, 将Encode和Decode方法写入其中, 这里给出Encode与Decode相应的代码:
codec.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 38 39 40 41 42 43 44 45 46 47
| package codec
import ( "bufio" "bytes" "encoding/binary" )
func Encode(message string) ([]byte, error) { var length int32 = int32(len(message)) var pkg *bytes.Buffer = new(bytes.Buffer) err := binary.Write(pkg, binary.LittleEndian, length) if err != nil { return nil, err } err = binary.Write(pkg, binary.LittleEndian, []byte(message)) if err != nil { return nil, err }
return pkg.Bytes(), nil }
func Decode(reader *bufio.Reader) (string, error) { lengthByte, _ := reader.Peek(4) lengthBuff := bytes.NewBuffer(lengthByte) var length int32 err := binary.Read(lengthBuff, binary.LittleEndian, &length) if err != nil { return "", err } if int32(reader.Buffered()) < length+4 { return "", err }
pack := make([]byte, int(4+length)) _, err = reader.Read(pack) if err != nil { return "", err } return string(pack[4:]), nil }
|
本文引自这里