网络编程
go这门语言由于它有gorountine和channel非常适合web编程,能够实现很高的并发。
网络编程本质上是利用的socket,底层借助的是操作系统提供的一系列能力;当然go自身也做了很多优化;
go提供的网络编程总体而言,和其它语言差别不大,也比较好懂;go屏蔽了底层的复杂性,将复杂留给自己,将简单留给使用者,真的很不错👍!
下面我们一步步看看go中网络编程的实现,本篇涉及的代码相对比较多,请耐心看哦!
1. TCP实现
1.1 简单TCP服务端-客户端
- 服务端
go
package main
import (
"fmt"
"net"
)
func main() {
// 1. 直接listen
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("listen error: ", err)
return
}
defer listen.Close()
// 打印出监听的端口信息
fmt.Println("listening on ", listen.Addr().String())
for {
// 2. Accept接收客户端请求
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
continue // 跳过本次 继续下一次
}
// 3. 开一个协程 单独处理连接
go handelConn(conn)
}
}
// 处理接收到的连接
// net.Conn是一个接口,拥有Read和Write方法
func handelConn(conn net.Conn) {
// 准备从conn中读取数据 存储的切片
data := make([]byte, 512)
// 返回读取的字节数,错误
n, err := conn.Read(data)
if err != nil {
fmt.Println("Read error: ", err)
return
}
fmt.Printf("读取到客户端ip: %s, 长度为:%v的信息: %s\n", conn.RemoteAddr(), n, string(data))
// 回写客户端消息
_, err = conn.Write([]byte("我已经收到你的请求!"))
if err != nil {
fmt.Println("write error: ", err)
return
}
}
// listening on [::]:8080
// 读取到客户端ip: [::1]:51644, 长度为:11的信息: hello world
- 客户端
go
package main
import (
"fmt"
"net"
)
func main() {
// 1. 客户端拨号 tcp ip:port
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("Dial error: ", err)
return
}
defer conn.Close()
// 2. 发送数据
_, err = conn.Write([]byte("hello world"))
if err != nil {
fmt.Println("Write error: ", err)
return
}
// 3.读取数据
data := make([]byte, 512)
n, err := conn.Read(data)
if err != nil {
fmt.Println("Read error: ", err)
return
}
fmt.Printf("Read data length: %d, content: %s", n, data)
}
// Read data length: 30, content: 我已经收到你的请求!%
1.2粘包问题
上诉的代码虽然能正常工作,但是如果客户端一次性发送很多信息的时候,会有粘包问题,我们一起来看下,怎么模拟呢?
- 把客户端改成循环发送很多次(比如60次)
- 服务端读取客户端信息改成循环读取
- 客户端
go
package main
import (
"fmt"
"net"
"strings"
)
func main() {
// 1. 客户端拨号 tcp ip:port
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("Dial error: ", err)
return
}
defer conn.Close()
// 构建一个很长的字符串 由60个 hellow world组成
str := strings.Repeat("hello world", 60)
// 2. 发送数据
_, err = conn.Write([]byte(str))
if err != nil {
fmt.Println("Write error: ", err)
return
}
}
- 服务端
go
package main
import (
"fmt"
"net"
)
func main() {
// 1. 直接listen
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("listen error: ", err)
return
}
defer listen.Close()
// 打印出监听的端口信息
fmt.Println("listening on ", listen.Addr().String())
for {
// 2. Accept接收客户端请求
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
continue // 跳过本次 继续下一次
}
// 3. 开一个协程 单独处理连接
go handelConn(conn)
}
}
// 处理接收到的连接
// net.Conn是一个接口,拥有Read和Write方法
func handelConn(conn net.Conn) {
// 准备从conn中读取数据 存储的切片
data := make([]byte, 512)
for {
// 返回读取的字节数,错误
n, err := conn.Read(data)
if err != nil {
fmt.Println("Read error: ", err)
return
}
fmt.Printf("读取到客户端ip: %s, 长度为:%v的信息: %s\n", conn.RemoteAddr(), n, string(data))
}
}
运行后会输出如下:
shell
✘ dongmingyan@sc ⮀ ~/go_playground/play ⮀ go run main.go
listening on [::]:8080
读取到客户端ip: [::1]:52266, 长度为:512的信息: hello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello
读取到客户端ip: [::1]:52266, 长度为:148的信息: worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello world worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello worldhello
Read error: EOF
从输出结果中我们可以看出,客户端一次写入的内容,在服务端分成了两次读取,且断句分割异常比如这里148的信息: worldhello
,这就是我们要说的粘包问题。
之所以分成了两次是因为,我们服务端指定了一次最大读取的长度为512个字节,纠其原因是因为我们不知道一次读多少长度的内容,如果知道长度那么就不会有这个问题。
粘包问题的本质是:服务端读取数据时,不知道要如何分割数据内容
1.3 解决粘包问题
解决粘包问题有很多种方式,比如:
- 约定一个固定长度,发送和接收都按照这个长度
- 使用分割符号,每次消息末尾加分割符,比如换行符号
- 在数据包中一个固定长度用于存储本次消息的长度,接收方法解析出来后根据它去读取消息
这里采用第三种方式演示:
- 我们约定数据的前4个字节(32位)存储本次消息长度
- 接收方读取数据时,先读取前4个字节获取到消息长度,然后读取具体内容
解决粘包主要是出来数据的发送和接收,主要代码如下
go
// 发送数据
func send(conn net.Conn, msg string) error {
// 出于性能考虑 添加buffer
buffer := bufio.NewWriter(conn)
// 计算信息长度 用4个字节 32位表示
length := uint32(len(msg))
// 写入消息长度 采用大端序
err := binary.Write(buffer, binary.BigEndian, length)
if err != nil {
return err
}
// 写入消息内容
err = binary.Write(buffer, binary.BigEndian, []byte(msg))
if err != nil {
return err
}
err = buffer.Flush() // 写入conn
return err
}
// 接收消息
func receive(conn net.Conn) (string, uint32, error) {
// 出于性能考虑 用buffer
buffer := bufio.NewReader(conn)
// 准备接收消息长度数据
var length uint32
// 从中读取长度
err := binary.Read(buffer, binary.BigEndian, &length)
if err != nil {
return "", length, err
}
// 准备读取信息
data := make([]byte, length)
// 从中读取具体信息
err = binary.Read(buffer, binary.BigEndian, data)
if err != nil {
return "", length, err
}
return string(data), length, nil
}
完整代码如下:
- 客户端:
go
package main
import (
"bufio"
"encoding/binary"
"fmt"
"net"
"strings"
)
func main() {
// 1. 客户端拨号 tcp ip:port
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("Dial error: ", err)
return
}
defer conn.Close()
// 构建一个很长的字符串 由60个 hellow world组成
str := strings.Repeat("hello world", 60)
// 2. 发送数据
err = send(conn, str)
if err != nil {
fmt.Println("Write error: ", err)
return
}
}
// 发送数据
func send(conn net.Conn, msg string) error {
// 出于性能考虑 添加buffer
buffer := bufio.NewWriter(conn)
// 计算信息长度 用4个字节 32位表示
length := uint32(len(msg))
// 写入消息长度 采用大端序
err := binary.Write(buffer, binary.BigEndian, length)
if err != nil {
return err
}
// 写入消息内容
err = binary.Write(buffer, binary.BigEndian, []byte(msg))
if err != nil {
return err
}
err = buffer.Flush() // 写入conn
return err
}
- 服务端
go
package main
import (
"bufio"
"encoding/binary"
"fmt"
"net"
)
func main() {
// 1. 直接listen
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("listen error: ", err)
return
}
defer listen.Close()
// 打印出监听的端口信息
fmt.Println("listening on ", listen.Addr().String())
for {
// 2. Accept接收客户端请求
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept error: ", err)
continue // 跳过本次 继续下一次
}
// 3. 开一个协程 单独处理连接
go handelConn(conn)
}
}
// 处理接收到的连接
// net.Conn是一个接口,拥有Read和Write方法
func handelConn(conn net.Conn) {
for {
// 返回消息内容 错误
msg, n, err := receive(conn)
if err != nil {
fmt.Println("Read error: ", err)
return
}
fmt.Printf("读取到客户端ip: %s, 长度为:%v的信息: %s\n", conn.RemoteAddr(), n, msg)
}
}
// 接收消息
func receive(conn net.Conn) (string, uint32, error) {
// 出于性能考虑 用buffer
buffer := bufio.NewReader(conn)
// 准备接收消息长度数据
var length uint32
// 从中读取长度
err := binary.Read(buffer, binary.BigEndian, &length)
if err != nil {
return "", length, err
}
// 准备读取信息
data := make([]byte, length)
// 从中读取具体信息
err = binary.Read(buffer, binary.BigEndian, data)
if err != nil {
return "", length, err
}
return string(data), length, nil
}
具体执行结果可以自行尝试。
2. UDP实现
udp相对于tcp更简单点,在服务端直接listen后就可以读写 listen使用 listenUDP
- 读取消息使用
ReadFromUDP
- 发送消息使用
WriteToUDP
- 服务端
go
package main
import (
"fmt"
"net"
)
func main() {
// 1. listenUDP 这里第二个参数需要UDPAddr
conn, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IPv4(0, 0, 0, 0),
Port: 8080,
})
if err != nil {
fmt.Println("listenUDP failed: ", err)
return
}
defer conn.Close()
fmt.Printf("listenUDP on: %s\n", conn.LocalAddr())
data := make([]byte, 512)
for {
// 2. 使用ReadFromUDP 读取,第二个参数返回的远端的udp地址
n, addr, err := conn.ReadFromUDP(data)
if err != nil {
fmt.Println("read failed: ", err)
continue
}
fmt.Printf("read addr: %s length: %d, content: %s\n", addr, n, data)
// 3. 使用WriteToUDP写 第二个参数需要是会写的地址
_, err = conn.WriteToUDP([]byte("收到消息"), addr)
if err != nil {
fmt.Println("write failed: ", err)
continue
}
}
}
- 客户端
go
package main
import (
"fmt"
"net"
)
func main() {
// 1. Dial 相比于tcp 这里改成udp
conn, err := net.Dial("udp", "localhost:8080")
if err != nil {
fmt.Println("udp Dial failed: ", err)
return
}
defer conn.Close()
// 2. 读写都和tcp一样
_, err = conn.Write([]byte("hello server!"))
if err != nil {
fmt.Println("udp Write failed: ", err)
return
}
// 3. 读和tcp一样
data := make([]byte, 512)
_, err = conn.Read(data)
if err != nil {
fmt.Println("udp Read failed: ", err)
return
}
fmt.Printf("udp read from server,content: %s\n", data)
}
3. 总结
- 从客户端看,无论是
tcp
和udp
使用上是一样的,都是通过net.Dial
、Write
、Read
完成,区别在于dial的时候类型是tcp
还是dup
- 从服务端看,区别稍大
- 对于tcp需要有两步,第一步
listen
,第二部Accept
,但是udp只有listen - 在读写上,
tcp
用Write
和Read
,而udp
用WriteToUDP
、ReadFromUDP
稍微繁琐了点。
- 对于tcp需要有两步,第一步
4. 聊天室功能实战
上面我们虽然进行了些练习,但是实战意义不大,这里我们写一个稍微贴近点的实战。
通过tcp实现,功能如下:
- 当有客户端加入,通知所有客户端
- 当有客户端退出,通知所有客户端
这段代码比较多,当然也不够完善,纯属练手,看看就好!
- 服务端
go
package main
import (
"bufio"
"encoding/binary"
"fmt"
"log"
"net"
)
// 定义客户端结构体
type client struct {
name string // 客户端名称
conn net.Conn // 这里存客户端的连接
}
// 存储客户端集合
var clients = make(map[string]*client)
// 向客户端发信息
func (c *client) sendMsg(msg string) error {
err := send(c.conn, msg)
return err
}
// 向客户端广播
func broadcast(msg string) {
for _, client := range clients {
err := client.sendMsg(msg)
if err != nil {
fmt.Println("broadcast error: ", err)
}
}
}
func main() {
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("listen error: ", err)
return
}
defer listen.Close()
log.Printf("chat server started, listening on: %s", listen.Addr().String())
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err: ", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
// 先获取客户端名,约定第一先填写客户端名称
name, _, err := receive(conn)
if err != nil {
fmt.Println("receive error: ", err)
return
}
defer conn.Close()
defer userLeave(name) // 用户离开
// 将连接存入clients中
clients[name] = &client{name: name, conn: conn}
// 广播加入消息
broadcast(fmt.Sprintf("欢迎 【%s】加入聊天", name))
for {
msg, _, err := receive(conn)
if err != nil {
fmt.Println("receive error: ", err)
return
}
broadcast(fmt.Sprintf("【%s】发言: %s", name, msg))
}
}
// 用户退出
func userLeave(name string) {
// 从clients中删除用户
delete(clients, name)
broadcast(fmt.Sprintf("【%s】退出聊天", name))
}
// 发送数据
func send(conn net.Conn, msg string) error {
// 出于性能考虑 添加buffer
buffer := bufio.NewWriter(conn)
// 计算信息长度 用4个字节 32位表示
length := uint32(len(msg))
// 写入消息长度 采用大端序
err := binary.Write(buffer, binary.BigEndian, length)
if err != nil {
return err
}
// 写入消息内容
err = binary.Write(buffer, binary.BigEndian, []byte(msg))
if err != nil {
return err
}
err = buffer.Flush() // 写入conn
return err
}
// 接收消息
func receive(conn net.Conn) (string, uint32, error) {
// 出于性能考虑 用buffer
buffer := bufio.NewReader(conn)
// 准备接收消息长度数据
var length uint32
// 从中读取长度
err := binary.Read(buffer, binary.BigEndian, &length)
if err != nil {
return "", length, err
}
// 准备读取信息
data := make([]byte, length)
// 从中读取具体信息
err = binary.Read(buffer, binary.BigEndian, data)
if err != nil {
return "", length, err
}
return string(data), length, nil
}
- 客户端
go
package main
import (
"bufio"
"encoding/binary"
"fmt"
"log"
"net"
)
// 定义客户端结构体
type client struct {
name string // 客户端名称
conn net.Conn // 这里存客户端的连接
}
// 存储客户端集合
var clients = make(map[string]*client)
// 向客户端发信息
func (c *client) sendMsg(msg string) error {
err := send(c.conn, msg)
return err
}
// 向客户端广播
func broadcast(msg string) {
for _, client := range clients {
err := client.sendMsg(msg)
if err != nil {
fmt.Println("broadcast error: ", err)
}
}
}
func main() {
listen, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("listen error: ", err)
return
}
defer listen.Close()
log.Printf("chat server started, listening on: %s", listen.Addr().String())
for {
conn, err := listen.Accept()
if err != nil {
fmt.Println("accept err: ", err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
// 先获取客户端名,约定第一先填写客户端名称
name, _, err := receive(conn)
if err != nil {
fmt.Println("receive error: ", err)
return
}
defer conn.Close()
defer userLeave(name) // 用户离开
// 将连接存入clients中
clients[name] = &client{name: name, conn: conn}
// 广播加入消息
broadcast(fmt.Sprintf("欢迎 【%s】加入聊天", name))
for {
msg, _, err := receive(conn)
if err != nil {
fmt.Println("receive error: ", err)
return
}
broadcast(fmt.Sprintf("【%s】发言: %s", name, msg))
}
}
// 用户退出
func userLeave(name string) {
// 从clients中删除用户
delete(clients, name)
broadcast(fmt.Sprintf("【%s】退出聊天", name))
}
// 发送数据
func send(conn net.Conn, msg string) error {
// 出于性能考虑 添加buffer
buffer := bufio.NewWriter(conn)
// 计算信息长度 用4个字节 32位表示
length := uint32(len(msg))
// 写入消息长度 采用大端序
err := binary.Write(buffer, binary.BigEndian, length)
if err != nil {
return err
}
// 写入消息内容
err = binary.Write(buffer, binary.BigEndian, []byte(msg))
if err != nil {
return err
}
err = buffer.Flush() // 写入conn
return err
}
// 接收消息
func receive(conn net.Conn) (string, uint32, error) {
// 出于性能考虑 用buffer
buffer := bufio.NewReader(conn)
// 准备接收消息长度数据
var length uint32
// 从中读取长度
err := binary.Read(buffer, binary.BigEndian, &length)
if err != nil {
return "", length, err
}
// 准备读取信息
data := make([]byte, length)
// 从中读取具体信息
err = binary.Read(buffer, binary.BigEndian, data)
if err != nil {
return "", length, err
}
return string(data), length, nil
}