2 服务器端与客户端开发范例
当我们在进行服务器端和客户端开发的时候需要注意,服务器端和客户端不能同时读取或者说同时写入数据,这样会造成死锁状态
由此我们需要约定到底是 server 端先读取或者说先写入,还是由 client 端先读取或者先写入,这个过程是可以约定的
所以当我们在做客户端与服务器端开发的话需要注意:
-
谁先读还是谁先写
-
一般规定,客户都先写,服务器端先读,读写循环交替
2.1 TCP 服务端与客户端开发流程
服务器端:
➢ 创建监听服务
➢ 循环接受客户端连接
➢ 数据处理(向客户端发送数据/读客户端发送的数据)
➢ 关闭监听服务
// 服务器五个步骤:
// 1.监听服务使用 net.Listen 函数
func net.Listen(network string, address string) (net.Listener, error)
// 2.接收客户端请求使用 Accept() 方法,返回链接 conn 接口
func (net.Listener).Accept() (net.Conn, error)
// 3.与客户端交换数据
// 4.关闭客户端
net.Conn.Close()
// 5.当服务器都处理完成之后关闭服务器,但是工作中一般不关闭
net.Listen.Close()
客户端:
➢ 连接服务器
➢ 数据处理(向服务端发送数据/读服务端发送的数据)
➢ 关闭连接
// 1.创建链接,第一个参数是网络协议,第二参数地址,返回 conn 链接 和一个 err
func net.Dial(network string, address string) (net.Conn, error)
// 2.交换数据
// 3.关闭链接
2.1.1 服务器客户端端未传输数据范例
服务器端范例代码
package main
import (
"fmt"
"net"
)
func main() {
// host:port
// 127.0.0.1 一般是提供给本机的程序间来访问,这是监听本地的 8888 端口
// 如果需要外部访问的话要监听能够让外部通讯的 ip ,或者 0.0.0.0 网段该 ip 指的是当前机器的所有网卡
addr := "0.0.0.0:8888"
// 协议使用 tcp
protocol := "tcp"
// 1.监听服务
// 第一个参数传入协议,第二个参数传入地址
// 返回一个 链接状体接口,和 err 信息,err 一般在端口被别的进程监听的时候报错,端口被占用
listener, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(listener.Addr())
// 2.当程序监听成功以后接收客户端请求,监听失败直接退出程序
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
} else {
// 3.与客户端交换数据
// 客户端链接成功之后打印 conn.LocalAddr() 本地地址, conn.RemoteAddr()远程地址客户端使用的 ip 和端口 这两个方法
fmt.Println(conn.LocalAddr(), conn.RemoteAddr())
// 4.关闭客户端链接
conn.Close()
}
// 5.关闭服务器
listener.Close()
}
客户端范例:
package main
import (
"fmt"
"net"
)
func main() {
// 客户端链接端口和 ip ,由于是本地程序开启的 8888 端口连接本机即可
addr := "127.0.0.1:8888"
// 链接协议
protocol := "tcp"
// 1.创建链接
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 2.交换数据,这里先未传输数据
// 3.关闭链接
conn.Close()
}
最后开启 server 和 client
-
通过执行我们可以看到,先启动 server 端之后会监听本地的 8888 端口
-
然后再启动客户端,再服务器终端上会显示本地监听端口和 ip 以及 远程客户端连接的 ip 和端口
2.1.2 服务器端接收客户端写入数据范例
客户端和服务器端都需要使用到下面两个方法
// 接收读取方法
Read()
// 发送写入方法
Write()
服务器端
package main
import (
"fmt"
"net"
)
func main() {
// host:port
// 127.0.0.1 一般是提供给本机的程序间来访问,这是监听本地的 8888 端口
// 如果需要外部访问的话要监听能够让外部通讯的 ip ,或者 0.0.0.0 网段该 ip 指的是当前机器的所有网卡
addr := "0.0.0.0:8888"
// 协议使用 tcp
protocol := "tcp"
// 1.监听服务
// 第一个参数传入协议,第二个参数传入地址
// 返回一个 链接状体接口,和 err 信息,err 一般在端口被别的进程监听的时候报错,端口被占用
listener, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(listener.Addr())
// 2.当程序监听成功以后接收客户端请求,监听失败直接退出程序
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
} else {
// 3.与客户端交换数据
// 客户端链接成功之后打印 conn.LocalAddr() 本地地址, conn.RemoteAddr()远程地址客户端使用的 ip 和端口 这两个方法
fmt.Println(conn.LocalAddr(), conn.RemoteAddr())
// 服务器端先接收
ctx := make([]byte, 1024)
n, err := conn.Read(ctx)
fmt.Println(string(ctx[:n]), n, err)
// 服务器端再写入数据
// conn.Write()
// 4.关闭客户端链接
conn.Close()
}
// 5.关闭服务器
listener.Close()
}
客户端
package main
import (
"fmt"
"net"
)
func main() {
// 客户端链接端口和 ip ,由于是本地程序开启的 8888 端口连接本机即可
addr := "127.0.0.1:8888"
// 链接协议
protocol := "tcp"
// 1.创建链接
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 2.交换数据
// 客户端先写入数据,这里写入 你好
conn.Write([]byte("你好"))
// conn.Read()
// 3.关闭链接
conn.Close()
}
开启 server 然后在 开启 client
-
客户端写入
[]byte
数据类型的 你好 -
客户端读取 1024 字节的数据,并转为 string 类型,你好 为 6 个字节,返回错误为 nil
但是我们这样做会发现,服务器端接收的数据字节数量是有限的,也就是说当我们的客户端传输的数据超过了服务器端接收的数据范围,服务器端就不会将数据全部接收。由此引出下面范例
2.1.2.1 约定读取和写入范围范例
类似于应用层协议编码解码过程,发送数据用编码器,接收数据用解码器
读取的时候
前 5 个字节 长度 99999
读 5 个字节 -> string -> int
ctx make([]byte,5) // 长度为 5 的 byte 切片
写入的时候
写 -> 前五个字节 => length fmt.Sprintf("%5d")
字符串还可以用这种方法
字符串:不包含换行
发送:保证以换行结尾
接收:带缓冲 IO ,ReadString("\n")
服务器端范例
package main
import (
"fmt"
"net"
"strconv"
"time"
)
// 发送数据编码器函数
func wrtie(conn net.Conn, txt string) error {
length := len(txt)
// 写入定义发送报文头部长度
// 由于定义了发送长度 length 所以 conn.Wrtie 返回的长度就可忽略
_, err := conn.Write([]byte(fmt.Sprintf("%05d", length)))
if err != nil {
fmt.Println(err)
return err
}
// 发送数据内容
_, err = conn.Write([]byte(txt))
if err != nil {
fmt.Println(err)
return err
}
return nil
}
// 接收数据解码器
func read(conn net.Conn) (string, error) {
// 先读取就定义一个长度为 5 的 byte 切片
lengthBytes := make([]byte, 5)
// 由于 lengthBytes 定义了接收数据长度,获取到接收数据
_, err := conn.Read(lengthBytes)
if err != nil {
fmt.Println(err)
return "", err
}
// 将获取到的接收数据转为 int ,传递给 length 变量
length, err := strconv.Atoi(string(lengthBytes))
if err != nil {
fmt.Println(err)
return "", err
}
// length 变量长度已经有了就可以定义 ctx 长度来接收
// 已经定义接收的长度 ctx 变量
ctx := make([]byte, length)
_, err = conn.Read(ctx)
if err != nil {
fmt.Println(err)
return "", err
}
// 由于已经定义了接收的数据长度,所以直接返回输出 string(ctx)
return string(ctx), nil
}
func main() {
// 监听所有网段的 8888 端口
addr := "0.0.0.0:8888"
// 协议使用 tcp
protocol := "tcp"
// 1.监听服务
listener, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(listener.Addr())
// 2.当程序监听成功以后接收客户端请求,监听失败直接退出程序
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
} else {
// 3.与客户端交换数据
fmt.Println(conn.LocalAddr(), conn.RemoteAddr())
// 服务器端读取客户端数据
fmt.Println(read(conn))
// 服务器端往客户端写入数据
wrtie(conn, time.Now().Format("2006-01-02 15:04:05"))
// 4.关闭客户端链接
conn.Close()
}
// 5.关闭服务器
listener.Close()
}
客户端范例:
package main
import (
"fmt"
"net"
"strconv"
)
// 发送数据编码器函数
func wrtie(conn net.Conn, txt string) error {
length := len(txt)
// 写入定义发送报文头部长度
// 由于定义了发送长度 length 所以 conn.Wrtie 返回的长度就可忽略
_, err := conn.Write([]byte(fmt.Sprintf("%05d", length)))
if err != nil {
fmt.Println(err)
return err
}
// 发送数据内容
_, err = conn.Write([]byte(txt))
if err != nil {
fmt.Println(err)
return err
}
return nil
}
// 接收数据解码器
func read(conn net.Conn) (string, error) {
// 先读取就定义一个长度为 5 的 byte 切片
lengthBytes := make([]byte, 5)
// 由于 lengthBytes 定义了接收长度,获取到接收数据
_, err := conn.Read(lengthBytes)
if err != nil {
fmt.Println(err)
return "", err
}
// 将获取到的接收数据转为 int ,传递个 length 变量
length, err := strconv.Atoi(string(lengthBytes))
if err != nil {
fmt.Println(err)
return "", err
}
// length 变量长度已经有了就可以定义 ctx 长度来接收
// 已经定义接收的长度 ctx 变量
ctx := make([]byte, length)
_, err = conn.Read(ctx)
if err != nil {
fmt.Println(err)
return "", err
}
// 由于已经定义了接收的数据长度,所以直接返回输出 string(ctx)
return string(ctx), nil
}
func main() {
addr := "127.0.0.1:8888"
protocol := "tcp"
// 1.创建链接
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 2.交换数据
// 向服务器端发送你好啦啦啦啦啦,并且读取服务器端发送过来的数据
wrtie(conn, "你好啦啦啦啦啦")
fmt.Println(read(conn))
// 3.关闭链接
conn.Close()
}
-
执行,服务器端开启 8888 端口
-
客户端往服务器端发送 你好啦啦啦啦啦
-
服务器端往客户端发送 当前时间
如果我们想服务器端和客户端之间重复的交换数据,只需在读取和发送的时候加一个 for 循环即可
服务器端重复 5 次读取和发送当前时间给客户端
客户端重复 5 次读取服务器端发送的数据和发送 你好啦啦啦啦啦 给服务器端
执行发送 5 次
2.1.2.2 服务器和客户端从终端输入数据发送,并实现退出功能
现在我想把这个程序做成一个客户端和服务器端,类似于一个聊天系统
server端范例
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
)
// 发送数据编码器函数
func wrtie(conn net.Conn, txt string) error {
length := len(txt)
// 写入定义发送报文头部长度
// 由于定义了发送长度 length 所以 conn.Wrtie 返回的长度就可忽略
_, err := conn.Write([]byte(fmt.Sprintf("%05d", length)))
if err != nil {
fmt.Println(err)
return err
}
// 发送数据内容
_, err = conn.Write([]byte(txt))
if err != nil {
fmt.Println(err)
return err
}
return nil
}
// 接收数据解码器
func read(conn net.Conn) (string, error) {
// 先读取就定义一个长度为 5 的 byte 切片
lengthBytes := make([]byte, 5)
// 由于 lengthBytes 定义了接收数据长度,获取到接收数据
_, err := conn.Read(lengthBytes)
if err != nil {
fmt.Println(err)
return "", err
}
// 将获取到的接收数据转为 int ,传递给 length 变量
length, err := strconv.Atoi(string(lengthBytes))
if err != nil {
fmt.Println(err)
return "", err
}
// length 变量长度已经有了就可以定义 ctx 长度来接收
// 已经定义接收的长度 ctx 变量
ctx := make([]byte, length)
_, err = conn.Read(ctx)
if err != nil {
fmt.Println(err)
return "", err
}
// 由于已经定义了接收的数据长度,所以直接返回输出 string(ctx)
return string(ctx), nil
}
// 定义一个输入 input 函数
func input(prompt string) string {
fmt.Print(prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
return strings.TrimSpace(scanner.Text())
}
func main() {
// 监听所有网段的 8888 端口
addr := "0.0.0.0:8888"
// 协议使用 tcp
protocol := "tcp"
// 1.监听服务
listener, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(listener.Addr())
// 2.当程序监听成功以后接收客户端请求,监听失败直接退出程序
conn, err := listener.Accept()
if err != nil {
fmt.Println(err)
return
} else {
fmt.Println(conn.LocalAddr(), conn.RemoteAddr())
// 3.与客户端交换数据
for {
fmt.Print("客户端发送消息:")
// 服务器端读取客户端数据,并且处理异常信息
if data, err := read(conn); err != nil {
fmt.Println(err)
break
} else {
fmt.Println(data)
}
info := input("请输入聊天内容:")
if info == "exit" {
break
}
// 服务器端往客户端写入数据,并且处理异常信息
if err := wrtie(conn, info); err != nil {
if err != io.EOF {
fmt.Println(err)
}
break
}
}
conn.Close()
}
// 5.关闭服务器
listener.Close()
}
client 端范例
package main
import (
"bufio"
"fmt"
"io"
"net"
"os"
"strconv"
"strings"
)
// 发送数据编码器函数
func wrtie(conn net.Conn, txt string) error {
length := len(txt)
// 写入定义发送报文头部长度
// 由于定义了发送长度 length 所以 conn.Wrtie 返回的长度就可忽略
_, err := conn.Write([]byte(fmt.Sprintf("%05d", length)))
if err != nil {
fmt.Println(err)
return err
}
// 发送数据内容
_, err = conn.Write([]byte(txt))
if err != nil {
fmt.Println(err)
return err
}
return nil
}
// 接收数据解码器
func read(conn net.Conn) (string, error) {
// 先读取就定义一个长度为 5 的 byte 切片
lengthBytes := make([]byte, 5)
// 由于 lengthBytes 定义了接收长度,获取到接收数据
_, err := conn.Read(lengthBytes)
if err != nil {
fmt.Println(err)
return "", err
}
// 将获取到的接收数据转为 int ,传递给 length 变量
length, err := strconv.Atoi(string(lengthBytes))
if err != nil {
fmt.Println(err)
return "", err
}
// length 变量长度已经有了就可以定义 ctx 长度来接收
// 已经定义接收的长度为 length 的 ctx 变量
ctx := make([]byte, length)
_, err = conn.Read(ctx)
if err != nil {
fmt.Println(err)
return "", err
}
// 由于已经定义了接收的数据长度,所以直接返回输出 string(ctx)
return string(ctx), nil
}
// 定义一个输入 input 函数
func input(prompt string) string {
fmt.Print(prompt)
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
// strings.TrimSpace() 去掉前后空白字符
return strings.TrimSpace(scanner.Text())
}
func main() {
addr := "127.0.0.1:8888"
protocol := "tcp"
// 1.创建链接
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
for {
// 2.交换数据
// for 循环实现与服务器端长连接发送消息
info := input("请输入聊天内容:")
if info == "exit" {
break
}
// 向服务器端写入数据,并且处理错误信息
if err := wrtie(conn, info); err != nil {
if err != io.EOF {
fmt.Println(err)
}
break
}
// 读取服务器端发送的数据,并且处理错误信息
fmt.Print("服务器端响应:")
if data, err := read(conn); err != nil {
fmt.Println(err)
break
} else {
fmt.Println(data)
}
}
// 3.关闭链接
conn.Close()
}
-
开启 server 端
-
启动 client 端口
-
实现聊天
-
一方 exit 退出,另外一端也即将退出
这个就是一个简单的服务器端和客户端的聊天程序,但是仔细想想该程序只能够连接一次,服务器启动以后客户端处理完成之后就关闭了,那我们如何让服务器端长久的对客户端进行服务呢,就是说另一个人连接上了以后都能去处理聊天呢,
也就是说当客户端如果关闭聊天完成以后,客户端下次还能在链接和服务器端来进行通讯。
2.1.2.3 实现服务器端重复处理客户端请求
通过上面的案例我们会发现当前的聊天系统 server 端只能够接收一次客户端的请求就会关闭链接,所以我们需要优化,将他实现成接收完了客户端的请求并不退出,而是需要接收下一次客户端的请求,所以这次我们只需要修改 server 端的源码即可
我们只需要在监听客户端请求到关闭客户端请求的地方添加一个 for 循环,如下图所示
执行
-
先开启 server 端
-
开启 client 端
-
我们观察已经实现了 server 端的长连接,即使客户端再次退出也能够链接
但是现在这个程序依旧不够完善,因为它只能处理一个客户端请求,当我需要处理多个客户端请求的时候我们需要使用到并发创建链接。由此引出下面案例