go 网络编程
TCP
go
网络编程中,用的最多的包是 net
使用 net.Listen
创建一个 tcp
服务,使用 net.Dial
创建一个 tcp
客户端
tcp
是流传输协议,是面向连接的,是可靠的,是有序的,是基于字节流的,是全双工的
server
先来写 server
端的代码,使用 net.Listen
创建一个 tcp
服务,服务地址为 127.0.0.1:8080
每当有一个新的连接进来,就创建一个创建一个 conn
,然后可以通过这个连接进行数据的读写
我们把这数据的读写 handleConnection
放到一个 goroutine
中,这样就可以同时处理多个连接。每个连接创建一个线程是比较耗费资源的,但是每个连接创建一个 goroutine
对与资源的消耗其实还好
在 handlerConnection
中,数据读取使用 conn.Read
,数据写入使用 conn.Write
,这两个方法都是阻塞的,如果没有数据读取,就会一直阻塞在这里,直到有数据读取
由于 tcp
是基于字节流的,我们并不知道读取的数据的长度,所以 conn.Read
和 conn.Write
返回的都是字节数
tcp
读取的数据是字节,写入的数据也是字节,就需要创建一个 byte
的切片,然后读取到的数据放入到这个切片,将写入的数据放到这个切片
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
defer listen.Close()
if err != nil {
panic(err)
}
for {
conn, err := listen.Accept()
if err != nil {
panic(err)
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read error", err.Error())
}
// 由于 tcp 是基于字节流的,所以读取的数据是字节,我们并不知道读取的数据的长度,所以需要使用 n 来获取读取的数据长度
fmt.Println("read data: ", string(buf[:n]))
_, err = conn.Write([]byte(buf[:n]))
if err != nil {
fmt.Println("write error", err.Error())
}
}
client
server
端代码准备好之后,就开始写 client
端的代码,使用 net.Dial
创建一个 tcp
客户端,连接到 tcp
服务地址
然后通过 conn.Write
写入数据,通过 conn.Read
读取数据,客户端 conn.Read
和 conn.Write
是不阻塞的
在读取或者写入数据时,需要创建一个 byte
的切片,然后将数据写入到这个切片,读取的数据也是放到这个切片
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
_, err = conn.Write([]byte("hello world"))
if err != nil {
panic(err)
}
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
panic(err)
}
fmt.Printf("read data: %v\n", string(buf[:n]))
}
启动服务
先启动 server
服务,再启动 client
服务,我就可以看到:
client
端输出read data: hello world
server
端输出read data: hello world
问题
我们对上面 client
端的代码进行两次写入,再次启动 client
服务,代码如下
_, err = conn.Write([]byte("hello world"))
if err != nil {
panic(err)
}
_, err = conn.Write([]byte("hello world"))
if err != nil {
panic(err)
}
我们会看到 server
端输出的数据是:read data: hello worldhello world
为什么会这样呢?
这是因为 tcp
是流传输,每次写和读之间没有明确的分割,所以 server
端读取的数据是两次写入的数据的拼接
这个时候就需要我们自己对数据进行一些切分,我们可以自己定义每一个数据包之间的的分割符是啥,这里用 \n
来表示
数据包之间用 \n 区分
在 client
端的代码中,我们在每次写入数据的后面加上 \n
,这样 server
端就可以通过 \n
来切分数据包,代码如下
_, err = conn.Write([]byte("hello world\n"))
if err != nil {
panic(err)
}
_, err = conn.Write([]byte("hello world\n"))
if err != nil {
panic(err)
}
在 server
端读取数据 conn.Read
一次读取就能把所有的数据都能读出来,用 strings.Split
来切分数据包,然后遍历数据包,再写入数据
func handleConnection(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
fmt.Println("read error", err.Error())
}
packs := strings.Split(string(buf[:n]), "\n")
for _, s := range packs {
n, err := conn.Write([]byte(s + "\n"))
fmt.Println(string(s), n)
if err != nil {
fmt.Println("write error", err.Error())
}
}
}
然后启动服务运行时会发现,client
传输两次数据,server
端也会返回两次数据,但是 client
接收到的数据,有时候是一条,有时候又是两条
这是因为 client
也是只读取了一次,当在读取的时候,第二次的数据还没有过来,所以就只能读到一条数据
解决这个问题就是在读取数据包时按行读取,使用 bufio.NewReader
创建一个 reader
,然后使用 reader.ReadLine()
server
端代码如下:
func handleConnection(conn net.Conn) {
defer conn.Close()
defer func() {
e := recover()
if e != nil {
fmt.Printf("recovery panic: %v\n", e)
}
}()
// conn 是一个 io.Reader,所以可以使用 bufio.NewReader 创建一个 reader
reader := bufio.NewReader(conn)
for {
line, _, err := reader.ReadLine()
fmt.Printf("read data: %v\n", string(line))
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
_, err = conn.Write([]byte(string(line) + "\n"))
if err != nil {
panic(err)
}
}
}
client
端代码如下:
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
_, err = conn.Write([]byte("hello world\n"))
if err != nil {
panic(err)
}
_, err = conn.Write([]byte("hello world\n"))
if err != nil {
panic(err)
}
// conn 是一个 io.Reader,所以可以使用 bufio.NewReader 创建一个 reader
reader := bufio.NewReader(conn)
for {
line, _, err := reader.ReadLine()
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
fmt.Printf("response data: %v\n", string(line))
}
}
使用数据长度进行包拆分
tcp
是面向流传输的,我们不知道两次请求之间的间隙是啥,就需要加一些标识,比如:\n
我们还可以自己定义一些数据包,比如在每个数据包前写入一个 int64
的数字
把一个数字加到 byte
中,代码如下:
byteSlice := make([]byte, 8)
binary.BigEndian.PutUint64(byteSlice, uint64(len([]byte(input))))
conn.Write(byteSlice)
然后在写入要传输的数据,这里就不要换行符了,代码如下:
conn.Write([]byte("hello world"))
在 server
端读取数据时,先读取 int64
的数字,然后再读取数据,代码如下:
// 先读取数据长度
reqDataLengthSlice := make([]byte, 8)
_, err := conn.Read(reqDataLengthSlice)
// 读实际的内容
dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)
dataSlice := make([]byte, dataLength)
_, err = conn.Read(dataSlice)
client
端完整代码:
func main() {
conn, err := net.Dial("tcp", "127.0.0.1:8080")
if err != nil {
panic(err)
}
defer conn.Close()
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input := scanner.Text()
if input == "EOF" {
break
}
byteSlice := make([]byte, 8)
binary.BigEndian.PutUint64(byteSlice, uint64(len([]byte(input))))
_, err := conn.Write(byteSlice)
if err != nil {
panic(err)
}
_, err = conn.Write([]byte(input))
if err != nil {
panic(err)
}
// 读一个标志
reqDataLengthSlice := make([]byte, 8)
_, err = conn.Read(reqDataLengthSlice)
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
// 读实际的内容
dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)
dataSlice := make([]byte, dataLength)
_, err = conn.Read(dataSlice)
if err != nil {
panic(err)
}
fmt.Printf("response data: %v\n", string(dataSlice))
}
}
server
端完整代码:
func main() {
listen, err := net.Listen("tcp", "127.0.0.1:8080")
defer listen.Close()
if err != nil {
panic(err)
}
for {
conn, err := listen.Accept()
if err != nil {
panic(err)
}
go handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
defer func() {
e := recover()
if e != nil {
fmt.Printf("recovery panic: %v\n", e)
}
}()
for {
// 读一个标志
reqDataLengthSlice := make([]byte, 8)
_, err := conn.Read(reqDataLengthSlice)
if err != nil {
if err == io.EOF {
break
}
panic(err)
}
// 读实际的内容
dataLength := binary.BigEndian.Uint64(reqDataLengthSlice)
dataSlice := make([]byte, dataLength)
_, err = conn.Read(dataSlice)
if err != nil {
panic(err)
}
fmt.Printf("read data: %v\n", string(dataSlice))
// 写一个标志
byteSlice := make([]byte, 8)
binary.BigEndian.PutUint64(byteSlice, uint64(len(byteSlice)))
_, err = conn.Write(byteSlice)
if err != nil {
panic(err)
}
_, err = conn.Write(dataSlice)
if err != nil {
panic(err)
}
}
}
UDP
建立 upd
连接,可以使用 net.ListenUDP
,创建一个 udp
服务
udp
是无连接的,所以不需要 Accept
,直接读取数据就可以了
读取数据使用 listen.ReadFromUDP
,它会返回三个值,分别是:
n
:数据字节数addr
:数据来源的地址err
:错误信息
写入数据使用 listen.WriteToUDP
,如果传输的数据太长,我们可以使用 goroutine
去处理,这样就可以同时处理多个请求
server
完整代码如下:
func main() {
listen, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 8080,
})
defer listen.Close()
if err != nil {
panic(err)
}
for {
data := make([]byte, 1024)
n, addr, err := listen.ReadFromUDP(data)
fmt.Printf("read data: %v", string(data[:n]))
go func() {
_, err = listen.WriteToUDP([]byte(string(data[:n])), addr)
if err != nil {
panic(err)
}
}()
}
}
客户端连接服务,可以使用 net.DialUDP
,它会返回一个连接 conn
,但这个连接只是一个逻辑上的连接,帮助我们更方便的进行数据的读写
ReadFromUDP
和 Read
区别是,ReadFromUDP
会返回一个 Addr
,Read
不会返回 Addr
client
完整代码如下:
func main() {
conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
IP: net.ParseIP("127.0.0.1"),
Port: 8080,
})
if err != nil {
panic(err)
}
defer conn.Close()
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
input := scanner.Text()
if input == "EOF" {
break
}
n, err := conn.Write([]byte(input))
if err != nil {
panic(err)
}
resp := make([]byte, 1024)
n, err = conn.Read(resp)
if err != nil {
panic(err)
}
fmt.Printf("response data: %v\n", string(resp[:n]))
}
}
http
http
服务是比较简单的,使用 http.ListenAndServe
创建一个 http
服务,然后使用 http.HandleFunc
注册一个路由
server
端代码:
func main() {
http.HandleFunc("/ping", func(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("pong"))
})
http.ListenAndServe(":8080", nil)
}
客户端使用 http.Get
或者 http.Post
请求服务端
client
端代码:
func main() {
resp, err := http.Get("http://127.0.0.1:8080/ping")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Println("get failed, status code: " + strconv.Itoa(resp.StatusCode))
return
}
respData, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("read data failed, err: %v\n", err)
}
fmt.Println(string(respData))
}
路由匹配
go
自带的 http
服务,是不区分 Method
也就是说不区分 Get
还是 Post
路由: /ping
请求: /ping/xxx
不会匹配到 /ping 路由
路由: /ping/
请求: /ping/xxxx
会匹配到 /ping/ 路由
路由: /ping
路由: /ping/
路由: /ping/xxx
请求: /ping/xxx
会匹配到 /ping/xxx 路由
请求分发
http.HandleFunc
可以注册多个路由,然后根据请求的 Method
来分发请求,代码如下
func main() {
http.HandleFunc("/user", func(writer http.ResponseWriter, request *http.Request) {
switch request.Method {
case http.MethodGet:
getUser(writer, request)
case http.MethodPost:
createUser(writer, request)
}
})
http.ListenAndServe(":8080", nil)
}
func getUser(writer http.ResponseWriter, request *http.Request) {
resp, _ := json.Marshal(map[string]interface{}{
"code": 200,
"data": map[string]interface{}{
"name": "uccs",
"age": 18,
},
})
writer.Write(resp)
}
func createUser(writer http.ResponseWriter, request *http.Request) {
resp, _ := json.Marshal(map[string]interface{}{
"code": 200,
})
writer.Write(resp)
}
设置 Response Header
writer
是一个 http.ResponseWriter
,可以设置 Header
,代码如下:
func getUser(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.Write(resp)
}
设置 http
状态码:
writer.WriteHeader(http.StatusOK)
要注意的是,Header
必须在 Write
之前设置,否则设置的 Header
不会生效
获取 query 参数
获取 query
参数,可以通过 request.URL.Query().Get
方法,代码如下:
func getUser(writer http.ResponseWriter, request *http.Request) {
userId := request.URL.Query().Get("user_id")
}
获取 form 参数
获取 form
参数,首先需要使用 request.ParseForm
方法,解析参数,然后使用 request.PostForm.Get
方法获取参数,代码如下:
- 也可以使用
request.Form.Get
方法获取参数
func createUser(writer http.ResponseWriter, request *http.Request) {
err := request.ParseForm()
if err != nil {
writer.WriteHeader(http.StatusBadRequest)
return
}
name := request.PostForm.Get("name")
resp, _ := json.Marshal(map[string]interface{}{
"code": 200,
"data": map[string]interface{}{
"name": name,
},
})
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.Write(resp)
}
获取 body 参数
获取请求中的 body
参数,可以使用 io.ReadAll
方法,读取 request.Body
,然后在使用 json.Unmarshal
的方法将请求体给解析出来,代码如下:
func createUser(writer http.ResponseWriter, request *http.Request) {
body, err := io.ReadAll(request.Body)
if err != nil {
writer.WriteHeader(http.StatusBadRequest)
return
}
user := struct {
Name string `json:"name"`
Age int64 `json:"age"`
}{}
if err := json.Unmarshal(body, &user); err != nil {
writer.WriteHeader(http.StatusBadRequest)
return
}
resp, _ := json.Marshal(map[string]interface{}{
"code": 200,
"data": user,
})
writer.Header().Set("Content-Type", "application/json; charset=utf-8")
writer.Write(resp)
}
转载自:https://juejin.cn/post/7372235558222037033