[TOC]
Go 网络编程
网络是用来解决数据传输问题的不同电脑间的通讯
◆网络编程
➢TCP服务器/客户端开发
➢UDP服务器/客户端开发
➢命令行聊天室
◆web开发
➢HTTP协议
➢web应用开发
➢客户端开发
◆Web爬虫
➢HTML结构
➢Goquery
◆RPC
服务器端:
- 监听服务(打电话)
- 接收客户端链接(等待电话)
- 交换数据(通讯)
- 关闭客户端连接(挂电话)
- 关闭服务,一般服务器端都不会关闭服务器
客户端:
- 创建连接(拨打电话)
- 交换数据(拨通电话)
- 关闭连接(挂电话)
交换数据:
- 应用层数据格式(应用层协议)
1 net 包
在 go 中提供过了 net 包,net 包提供了对网络的支持。
net 包提供了对 socket 编程的支持,socket 编程分服务端和客户端编程,针对服务端可使用函数 Listen 创建监听服务,对于客户端可使用函数 Dial 连接服务器
1.1 net 包常用函数
常用函数
- Listen: 用于创建监听服务器
- ListenPacket:用于创建服务器端连接
- Dial: 用于创建与服务器连接
- JoinHostPort:连接地址和端口
- SplitHostPort:分割地址和端口
- LookupAddr:查找地址对应主机名
- LookupHost: 根据主机名查看地址
- ParseCIDR:解析 CIDR 格式 IP
1.1.1 JoinHostPort() 连接地址和端口
JoinHostPort:连接地址和端口
func net.JoinHostPort(host string, port string) string
将主机和端口组合成“host:port”形式的网络地址。
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.JoinHostPort("127.0.0.1", "8080"))
}
执行
[16:52:20 root@go codes]#go run net.go
127.0.0.1:8080
1.1.2 SplitHostPort() 分割地址和端口
SplitHostPort:分割地址和端口
func net.SplitHostPort(hostport string) (host string, port string, err error)
网络地址拆分为主机IP 和 主机 Port
正确格式:
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.SplitHostPort("127.0.0.1:8080"))
}
执行
[16:59:28 root@go codes]#go run net.go
127.0.0.1 8080 <nil>
假如说我们输入的是一个错误的格式
package main
import (
"fmt"
"net"
)
func main() {
// 这里我们将 ip:port 的格式写成 ip-port 是一个错误格式
fmt.Println(net.SplitHostPort("127.0.0.1-8080"))
}
执行
[17:01:08 root@go codes]#go run net.go
address 127.0.0.1-8080: missing port in address
# 地址127.0.0.1-8080:地址中缺少端口
1.1.3 LookupAddr() 查找地址对应主机名
LookupAddr:查找地址对应主机名
func net.LookupAddr(addr string) (names []string, err error)
// addr 是查地址的
// 返回的是一个主机名的切片和 err 信息
很多时候我们需要通过 ip 来找他的主机名,或者说通过 主机名 来找它的 ip ,这个过程其实就类似于 DNS 的过程,对给定地址执行反向查找,返回映射到该地址的名称列表。
正确范例:
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println(net.LookupAddr("127.0.0.1"))
}
执行
[17:01:33 root@go codes]#go run net.go
[localhost localhost.localdomain. localhost4 localhost4.localdomain4.] <nil>
# [localhost localhost.localdomain. localhost4 localhost4.localdomain4.] 返回的是主机头名称
[17:12:50 root@go codes]#cat /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
错误范例:
package main
import (
"fmt"
"net"
)
func main() {
// 这里写一个不存在的 ip
fmt.Println(net.LookupAddr("220.181.38.148"))
}
执行
[17:26:40 root@go codes]#go run net.go
[] lookup 79.69.156.39.in-addr.arpa. on 114.114.114.114:53: no such host
# []查找148.38.181.220.in-addr.arpa。114.114.114.114:53:没有这样的主机
1.1.4 LookupHost() 根据主机名查看地址
LookupHost: 根据主机名查看地址
func net.LookupHost(host string) (addrs []string, err error)
使用本地解析程序查找给定的主机。它返回该主机地址的一部分。
package main
import (
"fmt"
"net"
)
func main() {
// 查询百度
fmt.Println(net.LookupHost("www.baidu.com"))
// 查询本地
fmt.Println(net.LookupHost("localhost"))
}
执行
[17:27:37 root@go codes]#go run net.go
[110.242.68.3 110.242.68.4] <nil>
[127.0.0.1 ::1] <nil>
# 返回的就是一个地址列表和 error ,这个就是通过 DNS 解析
1.1.5 ParseIP() 判断是否为正确的 ip
ParseIP:判断是否为正确的 ip
func net.ParseIP(s string) net.IP
范例
package main
import (
"fmt"
"net"
)
func main() {
for _, ipStr := range []string{"127.0.0.1", "::1", "xxxx"} {
ip := net.ParseIP(ipStr)
fmt.Println(ip)
}
}
执行
[17:42:01 root@go codes]#go run net.go
127.0.0.1
::1
<nil> # 由于 xxxx 不是一个正确的 ip 地址所以返回值为 nil
1.1.6 ParseCIDR() 查看子网掩码
func net.ParseCIDR(s string) (net.IP, *net.IPNet, error)
查看子网掩码
package main
import (
"fmt"
"net"
)
func main() {
// 这里传入的是一个 192.168.1.1/24 的 24 位子网掩码
ip, ipnet, err := net.ParseCIDR("192.168.1.1/24")
fmt.Println(ip, ipnet, err)
}
执行
[17:47:19 root@go codes]#go run net.go
192.168.1.1 192.168.1.0/24 <nil>
# 192.168.1.1 ip 地址
# 192.168.1.0/24 子网掩码
# <nil> 错误为空
1.1.6.1 Contains() 方法查询该网段是否包含地址 ip
Contains() 方法是 net.ParseCIDR 函数中返回的一个 IPNet 结构体中的方法
Contains() 方法是用来报告该网段是否包含地址 ip
func (*net.IPNet).Contains(ip net.IP) bool
// Contains(ip net.IP) 传递 ip 格式
package main
import (
"fmt"
"net"
)
func main() {
// 这里写的是 192.168.1.1/24 网段
ip, ipnet, err := net.ParseCIDR("192.168.1.1/24")
fmt.Println(ip, ipnet, err)
// 由于是 24 位子网掩码,而这里写的是 192.168.0.1 他的第 16 位是 0 所以结果为 false
fmt.Println(ipnet.Contains(net.ParseIP("192.168.0.1")))
}
执行
[18:09:23 root@go codes]#go run net.go
192.168.1.1 192.168.1.0/24 <nil>
false
# false 由于 192.168.1.1/24 24位子网 不包含 192.168.0.1 地址
1.1.7 InterfaceAddrs() 返回系统的所有 ip
func net.InterfaceAddrs() ([]net.Addr, error)
InterfaceAddrs
返回系统的单播接口地址列表。
package main
import (
"fmt"
"net"
)
func main() {
addr, err := net.InterfaceAddrs()
fmt.Println(addr, err)
}
执行
[18:09:26 root@go codes]#go run net.go
[127.0.0.1/8 10.0.0.10/24 192.168.122.1/24] <nil>
# 127.0.0.1/8 10.0.0.10/24 192.168.122.1/24 本机网卡所有存在的 ip
# <nil> 返回值为空
1.1.8 Interfaces() 获取网卡信息
接口返回系统网络接口的列表比如 mac 地址。
func net.Interfaces() ([]net.Interface, error)
// 返回 []net.Interface 网卡接口信息切片
package main
import (
"fmt"
"net"
)
func main() {
intfs, _ := net.Interfaces()
// 遍历 intfs 网卡信息切片,输出值
for _, netinfo := range intfs {
fmt.Println(netinfo.Index, netinfo.MTU, netinfo.HardwareAddr,netinfo.Name, netinfo.Flags)
// 获取当前该网卡地址 ip
fmt.Println(netinfo.Addrs())
}
}
执行
[18:33:20 root@go codes]#go run net.go
1 65536 lo up|loopback
[127.0.0.1/8] <nil>
2 1500 00:0c:29:93:c6:1b eth0 up|broadcast|multicast
[10.0.0.10/24] <nil>
3 1500 52:54:00:ff:d5:b5 virbr0 up|broadcast|multicast
[192.168.122.1/24] <nil>
4 1500 52:54:00:ff:d5:b5 virbr0-nic broadcast|multicast
[] <nil>
# 1 lo 回环网卡信息
# 2 eth0 网卡信息
# 3 virbr0 网卡信息
# 4 virbr0-nic 网卡信息
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.与客户端交换数据
Read()
Writer()
// 4.关闭客户端
net.Conn.Close()
// 5.当服务器都处理完成之后关闭服务器,但是工作中一般不关闭
net.Listen.Close()
客户端:
➢ 连接服务器
➢ 数据处理(向服务端发送数据/读服务端发送的数据)
➢ 关闭连接
// 1.创建链接,第一个参数是网络协议,第二参数地址,返回 conn 链接 和一个 err
func net.Dial(network string, address string) (net.Conn, error)
// 2.交换数据
Read()
Writer()
// 3.关闭链接
conn.Close()
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)
// conn.Read 从链接读取数据
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("%05d")
通过 Sprintf("%05d") 起到占位符的效果
字符串还可以用这种方法
字符串:不包含换行
发送:保证以换行结尾 go
接收:带缓冲 IO ,ReadString("\n")
客户端服务器端读取数据的 read 函数操作
客户端服务器端读操作:
服务器端范例
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 端的长连接,即使客户端再次退出也能够链接
但是现在这个程序依旧不够完善,因为它只能处理一个客户端请求,当我需要处理多个客户端请求的时候我们需要使用到并发创建链接。由此引出下面案例
2.2 tcp 服务器并发处理客户端请求范例
并发:
时间服务器
当 client 链接,就给客户端响应,服务器端就给客户端返回一个当前时间
2.2.1 未作并发处理范例
server 端
package main
import (
"fmt"
"net"
"time"
)
func main() {
addr := "0.0.0.0:9999"
protocol := "tcp"
listens, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 循环处理客户端链接
for {
conn, err := listens.Accept()
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("客户端[%s]连接成功\n", conn.RemoteAddr())
// 这里暂停 10 秒用来模拟客户端链接的过程比较久
time.Sleep(10 * time.Second)
// 发送时间
fmt.Fprintln(conn, time.Now().Format("[2006-01-02 15:04:05]"))
// 关闭客户端链接
conn.Close()
fmt.Printf("客户端[%s]退出\n", conn.RemoteAddr())
}
// 关闭服务器端链接
listens.Close()
}
client 端
package main
import (
"bufio"
"fmt"
"net"
"time"
)
func main() {
addr := "127.0.0.1:9999"
protocol := "tcp"
// 记录客户端的连接时间
start := time.Now()
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 客户端读取一行服务器端数据
reader := bufio.NewReader(conn)
fmt.Println(reader.ReadString('\n'))
// 关闭连接
conn.Close()
// 记录客户端一共链接了多久
fmt.Println(time.Now().Sub(start))
}
执行 server 端
我们通过观察会发现,一个客户端请求连接上了需要等待 10s 才知道请求结果,同时启动第二个客户端需要等待 18s 才能得到服务器端的响应
但是这个时间服务器如果会被多个客户端使用的时候,这里我在开启第二个客户端的时候,在隔了 10s 才响应了第二个客户端,如果当前这个程序被一百个客户端访问,那么是不是越往后连接的客户端,服务器端处理的时间越久。
由此就引出了我们并发的请求,不可能说多个客户端来请求服务器端越往后的客户端处理的时间越长,这种显然是不合理的
2.2.2 并发处理范例
如果我们需要开启并发处理,问题一定在 server 端,如果说我接受一个客户端我们能不能通过协程来处理这个请求呢?
我们这个 server 端的问题主要是出在客户端链接这一块,我们可以将客户端连接这一块的代码封装为一个协程也就是开启 goroutine
修改后的 server 端代码
package main
import (
"fmt"
"net"
"time"
)
func main() {
addr := "0.0.0.0:9999"
protocol := "tcp"
listens, err := net.Listen(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 循环处理客户端链接
for {
conn, err := listens.Accept()
if err != nil {
fmt.Println(err)
continue
}
// 开启协程进行处理客户端请求
go func() {
fmt.Printf("客户端[%s]连接成功\n", conn.RemoteAddr())
// 这里暂停 10 秒用来模拟客户端链接的过程比较久
time.Sleep(10 * time.Second)
// 发送时间
fmt.Fprintln(conn, time.Now().Format("[2006-01-02 15:04:05]"))
// 关闭客户端链接
conn.Close()
fmt.Printf("客户端[%s]退出\n", conn.RemoteAddr())
}()
}
// 关闭服务器端链接
listens.Close()
}
执行
通过执行我们会发现现在 server 端能够通过处理多个 客户端请求,由此借助了 goroutine 的好处
goroutine 严格遵循了 MPG 工作原理一个 goroutine 支持多个并发请求处理[典藏版] Golang 调度器 GMP 原理与调度全分析 | Go 技术论坛 (learnku.com)
2.3 UDP 服务器和客户端开发
UDP 服务端开发流程
➢ 创建监听服务
➢ 数据处理(向客户端发送数据/读客户端发送的数据)
➢ 关闭连接
UDP 客户端开发流程
➢ 创建连接
➢ 数据处理(向服务器发送数据/读服务器发送的数据)
➢ 关闭连接
UDP 协议是非面向连接
UDP 其实是一种不可靠的协议,它在发送的时候不管客户端是否存在,也不管数据是否真的发送给了客户端。在工作中常见的 UDP 协议一般都是 DNS 在使用
在 UDP 种服务端开发和客户端开发都和 TCP 一样的。三个最主要的因素都是地址和协议还有链接
在 UDP 开发种没有监听
// 开启 UDP 链接
func net.ListenPacket(network string, address string) (net.PacketConn, error)
// 读取数据
func (net.PacketConn).ReadFrom(p []byte) (n int, addr net.Addr, err error)
2.3.1 服务器端不回复客户端消息范例
服务器端代码
package main
import (
"fmt"
"net"
)
func main() {
// 监听所有网卡的 8888 端口
addr := ":8888"
// 协议使用 udp
protocol := "udp"
// 启动 UDP 链接
packetConn, err := net.ListenPacket(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 处理客户端
// 通过 for 循环不断开连接读取客户端发来的数据,每次读取 1024 字节
for {
ctx := make([]byte, 1024)
// 读取数据的时候肯定是需要知道从谁读取的,我们肯定需要告诉服务器端吧
n, addr, err := packetConn.ReadFrom(ctx)
if err != nil {
fmt.Println(err)
continue
}
// 输出 客户端 发送的数据
fmt.Printf("客户端[%s]发送数据:%s\n", addr, string(ctx[:n]))
}
// 关闭服务器端
packetConn.Close()
}
客户端代码
package main
import (
"fmt"
"net"
"time"
)
func main() {
addr := "127.0.0.1:8888"
protocol := "udp"
// UDP 客户端链接 server 端也是使用 net.Dial()
conn, err := net.Dial(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 客户端往服务器端发送数据
n, err := conn.Write([]byte(time.Now().Format("2006-01-02 15:04:05")))
fmt.Println(n, err)
// 接收服务器端发送数据,因为 read 需要接收一个 []byte, 所以定义 cxt 的字节切皮变量
cxt := make([]byte, 1024)
// read 类似于一个寄存器,不接收到的数据寄存在 cxt 切片中,获取接收到的数据长度
n, err = conn.Read(cxt)
fmt.Printf("服务器端发送数据:%s\n", string(cxt[:n]))
// 关闭连接
conn.Close()
}
- 我们直接运行客户端
[17:47:26 root@go UDP]#go run client.go
21 <nil>
# 通过运行我们会发现即使没有启动 server 端,客户端也不会报错
# 因为 UDP 协议中客户端发送数据的时候并不会关心服务器端是否真的存在
- 接着我们这次先启动服务器端
- 先启动 server 端
- 在启动 client 端
但是这个时候服务器端并不能向客户端回复消息。所以还的优化 server 端代码
2.3.2 server 端向客户端回复消息范例
server 端需要向 客户端 回复消息要使用到 net.WriteTo()
方法,而且我们只需要修改 server 代码即可
[18:04:13 root@go UDP]#go doc net.writeTo
package net // import "net"
func (v *Buffers) WriteTo(w io.Writer) (n int64, err error)
func (c *IPConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo implements the PacketConn WriteTo method.
# 通过查看 go doc net.WriteTo() 方法是有两个参数一个是写入的字节切,一个是回复地址
func (c *UDPConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo implements the PacketConn WriteTo method.
func (c *UnixConn) WriteTo(b []byte, addr Addr) (int, error)
WriteTo implements the PacketConn WriteTo method.
server 端代码
package main
import (
"fmt"
"net"
)
func main() {
// 监听所有网卡的 8888 端口
addr := ":8888"
// 协议使用 udp
protocol := "udp"
// 启动 UDP 链接
packetConn, err := net.ListenPacket(protocol, addr)
if err != nil {
fmt.Println(err)
return
}
// 处理客户端
// 通过 for 循环读取数据,每次读取 1024 字节
for {
ctx := make([]byte, 1024)
// 读取数据的时候肯定是需要知道从谁读取的,我们肯定需要告诉服务器端吧
n, addr, err := packetConn.ReadFrom(ctx)
if err != nil {
fmt.Println(err)
continue
}
// 输出 客户端 发送的数据
fmt.Printf("客户端[%s]发送数据:%s\n", addr, string(ctx[:n]))
// 服务器端向客户端发送数据"我是服务器"
fmt.Println(packetConn.WriteTo([]byte(string("我是服务器")), addr))
}
// 关闭服务器端
packetConn.Close()
}
通过执行 server 端程序,而且 客户端未接收也不会报错,这个就是 UDP 和 TCP 协议之间的区别。
UDP 服务器端在发送数据的时候不会管客户端是否接收到
- 执行服务器端
- 执行客户端
- 客户端接收到了服务器端发送的数据