【Golang】网络开发:TCP

有关网络编程

网络编程在之前的文章中有提过,可以自行查看:

【swoole.1.02】初体验

【swoole.1.04】udp协议通信和粘包问题

使用GO完成TCP通信

net包

有关网络通信的相关处理需要使用到 net 包,包中内容比较多,下次再详细阅读,先来创建简单的tcp和udp网络通信吧

net包提供了可移植的网络I/O接口,包括TCP/IP、UDP、域名解析和Unix域socket。

虽然本包提供了对网络原语的访问,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口;以及相关的Conn和Listener接口。crypto/tls包提供了相同的接口和类似的Dial和Listen函数。

Listen函数创建的服务端:

ln, err := net.Listen("tcp", ":8080")
if err != nil {
	// handle error
}
for {
	conn, err := ln.Accept()
	if err != nil {
		// handle error
		continue
	}
	go handleConnection(conn)
}

初体验

TCP服务端程序的处理流程:

  1. 监听端口
  2. 接收客户端请求建立链接
  3. 处理发送过来的数据

TCP客户端程序的处理流程:

  1. 建立与服务端的链接
  2. 进行数据收发
  3. 关闭链接
写一个入口程序
const (
	TcpListenIp   string = "127.0.0.1"
	TcpListenPort string = "9900"
)

func main() {
	method := os.Args[1]
	log.Println(method)
	if method == "s" {
		log.Println("开始创建服务器")
		nets.TcpServer(TcpListenIp + ":" + TcpListenPort)
	} else {
		log.Println("开始创建客户端")
		nets.TcpClient(TcpListenIp + ":" + TcpListenPort)
	}
}

为了方便开发,将客户端和服务端分开在其他包不同文件中开发了。首先写一个入口程序,根据参数开启不懂的服务

服务端
package nets

import (
	"bufio"
	"log"
	"net"
)

func TcpServer(address string) {
	//	创建连接
	listener, err := net.Listen("tcp", address) // 监听端口连接
	if err != nil {
		log.Println("监听端口出错:", err)
	} else {
		log.Println("监听完成")
	}
	log.Println("开始阻塞监听连接事件")

	// 阻塞监听连接事件
	conn, err := listener.Accept() // 阻塞监听连接事件
	if err != nil {
		log.Println("接受socket连接出错:", err)
		return
	} else {
		log.Println("有新的连接")
	}

	// 数据交互过程
	for {
		reader := bufio.NewReader(conn) // 创建io缓冲区
		var buffer [128]byte
		n, err := reader.Read(buffer[:]) // 阻塞监听socket变化,读取数据
		if err != nil {
			log.Println("读取数据出错:", err)
			break
		}
		recvStr := string(buffer[:n])
		log.Println("收到客户端发来的数据:", recvStr)
		n, err = conn.Write(buffer[:n])
		if err != nil {
			log.Println("发送数据出错:", err)
		} else {
			log.Println("发送数据成功:", recvStr)
		}
	}
}

关键点:

func Listen(net, laddr string) (Listener, error)

返回在一个本地网络地址laddr上监听的Listener。网络类型参数net必须是面向流的网络:

"tcp"、"tcp4"、"tcp6"、"unix"或"unixpacket"。参见Dial函数获取laddr的语法。

创建一个地址的监听器

func (l *TCPListener) Accept() (Conn, error)

Accept用于实现Listener接口的Accept方法;他会等待下一个呼叫,并返回一个该呼叫的Conn接口。

监听连接事件

func NewReader(rd io.Reader) *Reader

NewReader创建一个具有默认大小缓冲、从r读取的*Reader。

创建一个缓冲区,为数据接收做准备

func (b *Reader) Read(p []byte) (n int, err error)

Read读取数据写入p。本方法返回写入p的字节数。本方法一次调用最多会调用下层Reader接口一次Read方法,因此返回值n可能小于len(p)。读取到达结尾时,返回值n将为0而err将为io.EOF。

读取缓冲区的数据

func (c *TCPConn) Write(b []byte) (int, error)

Write实现了Conn接口Write方法

向连接写入数据(发送数据包)

客户端
package nets

import (
	"bufio"
	"log"
	"net"
	"os"
	"strings"
)

func TcpClient(address string) {
	conn, err := net.Dial("tcp", address) // 创建一个连接用的客户端
	if err != nil {
		log.Println("err :", err)
		return
	} else {
		log.Println("创建客户端完成")
		defer conn.Close() // 关闭连接
	}
	inputReader := bufio.NewReader(os.Stdin) // 创建一个命令行输入的缓冲读取器
	for {
		log.Println("等待新的指令")
		input, _ := inputReader.ReadString('\n') // 读取用户输入
		inputInfo := strings.Trim(input, "\r\n")
		if strings.ToLower(inputInfo) == "exit" { // 输入exit退出
			return
		}
		_, err = conn.Write([]byte(inputInfo)) // 发送数据
		if err != nil {
			return
		} else {
			log.Println("已发送:", inputInfo)
		}
		buf := [512]byte{}
		log.Println("开始阻塞等待服务端返回数据")
		n, err := conn.Read(buf[:]) // 阻塞等待服务端返回
		if err != nil {
			log.Println("获取数据失败:", err)
			return
		} else {
			log.Println("已发送:", string(buf[:n]))
		}
	}
}

关键点:

func Dial(network, address string) (Conn, error)

在网络network上连接地址address,并返回一个Conn接口。可用的网络类型有:

"tcp"、"tcp4"、"tcp6"、"udp"、"udp4"、"udp6"、"ip"、"ip4"、"ip6"、"unix"、"unixgram"、"unixpacket"

两个客户端的输入和输出

服务端:

开始创建服务器
2020/03/17 14:26:00 监听完成
2020/03/17 14:26:00 开始阻塞监听连接事件
2020/03/17 14:26:04 有新的连接
2020/03/17 14:26:08 收到客户端发来的数据: msg
2020/03/17 14:26:08 发送数据成功: msg
2020/03/17 14:26:12 收到客户端发来的数据: msg1
2020/03/17 14:26:12 发送数据成功: msg1

客户端:

开始创建客户端
2020/03/17 14:26:04 创建客户端完成
2020/03/17 14:26:04 等待新的指令
msg
2020/03/17 14:26:08 已发送: msg
2020/03/17 14:26:08 开始阻塞等待服务端返回数据
2020/03/17 14:26:08 已发送: msg
2020/03/17 14:26:08 等待新的指令
msg1
2020/03/17 14:26:12 已发送: msg1
2020/03/17 14:26:12 开始阻塞等待服务端返回数据
2020/03/17 14:26:12 已发送: msg1
2020/03/17 14:26:12 等待新的指令

让一个服务端处理多个客户端连接

在上面的demo中,一个服务端仅支持一个客户端的交互。之前在swoole中有提到几种网络模型:预派生,select,poll,epoll

在go中,因为方便的协程切换,可以直接使用协程来处理多连接交互。使用协程处理客户端连接,在主进程中阻塞监听socket连接事件,在连接后将socket连接交给协程,由协程异步阻塞监听socket可读事件处理数据交互,这样的话就可以很方便的进行协程的创建和切换:

服务端

将连接后的处理扔到了协程中

package nets

import (
	"bufio"
	"log"
	"net"
)

func TcpServer(address string) {
	//	创建连接
	listener, err := net.Listen("tcp", address) // 监听端口连接
	if err != nil {
		log.Println("监听端口出错:", err)
	} else {
		log.Println("监听完成")
	}
	log.Println("开始阻塞监听连接事件")

	for {
		// 阻塞监听连接事件
		conn, err := listener.Accept() // 阻塞监听连接事件
		if err != nil {
			log.Println("接受socket连接出错:", err)
			return
		} else {
			log.Println("有新的连接")
		}
		go doRead(conn)
	}
}

func doRead(conn net.Conn)  {
	// 数据交互过程
	for {
		reader := bufio.NewReader(conn) // 创建io缓冲区
		var buffer [128]byte
		n, err := reader.Read(buffer[:]) // 阻塞监听socket变化,读取数据
		if err != nil {
			log.Println("读取数据出错:", err)
			break
		}
		recvStr := string(buffer[:n])
		log.Println("收到客户端发来的数据:", recvStr)
		n, err = conn.Write(buffer[:n])
		if err != nil {
			log.Println("发送数据出错:", err)
		} else {
			log.Println("发送数据成功:", recvStr)
		}
	}
}
客户端

增加了客户端标示

package nets

import (
	"bufio"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
	"time"
)

func TcpClient(address string) {
	var cname string
	conn, err := net.Dial("tcp", address) // 创建一个连接用的客户端
	if err != nil {
		log.Println("err :", err)
		return
	} else {
		cname = strconv.Itoa(int(time.Now().Unix()))
		log.Println("创建客户端完成,当前客户端名:", cname)
		defer func() {
			_, _ = conn.Write([]byte(cname + ": 我走了"))
			err := conn.Close() // 关闭连接
			if err != nil {
				log.Println("关闭连接失败:", err)
			} else {
				log.Println("退出了")
			}
		}()
	}
	inputReader := bufio.NewReader(os.Stdin) // 创建一个命令行输入的缓冲读取器
	for {
		log.Println("等待新的指令")
		input, _ := inputReader.ReadString('\n') // 读取用户输入
		inputInfo := strings.Trim(input, "\r\n")
		if strings.ToLower(inputInfo) == "exit" { // 输入exit退出
			return
		}
		_, err = conn.Write([]byte(cname + ": " + inputInfo)) // 发送数据
		if err != nil {
			return
		} else {
			log.Println("已发送:", inputInfo)
		}
		buf := [512]byte{}
		log.Println("开始阻塞等待服务端返回数据")
		n, err := conn.Read(buf[:]) // 阻塞等待服务端返回
		if err != nil {
			log.Println("获取数据失败:", err)
			return
		} else {
			log.Println("已发送:", string(buf[:n]))
		}
	}
}
输出

server:

2020/03/17 14:57:24 开始创建服务器
2020/03/17 14:57:24 监听完成
2020/03/17 14:57:24 开始阻塞监听连接事件
2020/03/17 14:57:26 有新的连接
2020/03/17 14:57:30 有新的连接
2020/03/17 14:57:32 有新的连接
2020/03/17 14:57:37 收到客户端发来的数据: 1584428246: 我是1
2020/03/17 14:57:37 发送数据成功: 1584428246: 我是1
2020/03/17 14:57:39 收到客户端发来的数据: 1584428250: 我是2
2020/03/17 14:57:39 发送数据成功: 1584428250: 我是2
2020/03/17 14:57:42 收到客户端发来的数据: 1584428252: 我是3
2020/03/17 14:57:42 发送数据成功: 1584428252: 我是3
2020/03/17 14:57:45 收到客户端发来的数据: 1584428246: 我走了
2020/03/17 14:57:45 发送数据成功: 1584428246: 我走了
2020/03/17 14:57:56 收到客户端发来的数据: 1584428250: 我走了
2020/03/17 14:57:56 发送数据成功: 1584428250: 我走了
2020/03/17 14:58:06 收到客户端发来的数据: 1584428252: 我走了
2020/03/17 14:58:06 发送数据成功: 1584428252: 我走了

client1:

2020/03/17 14:57:26 开始创建客户端
2020/03/17 14:57:26 创建客户端完成,当前客户端名: 1584428246
2020/03/17 14:57:26 等待新的指令
我是1
2020/03/17 14:57:37 已发送: 我是1
2020/03/17 14:57:37 开始阻塞等待服务端返回数据
2020/03/17 14:57:37 已发送: 1584428246: 我是1
2020/03/17 14:57:37 等待新的指令
exit
2020/03/17 14:57:45 退出了

client2:

2020/03/17 14:57:30 开始创建客户端
2020/03/17 14:57:30 创建客户端完成,当前客户端名: 1584428250
2020/03/17 14:57:30 等待新的指令
我是2
2020/03/17 14:57:39 已发送: 我是2
2020/03/17 14:57:39 开始阻塞等待服务端返回数据
2020/03/17 14:57:39 已发送: 1584428250: 我是2
2020/03/17 14:57:39 等待新的指令
exit
2020/03/17 14:57:56 退出了

client3

2020/03/17 14:57:32 开始创建客户端
2020/03/17 14:57:32 创建客户端完成,当前客户端名: 1584428252
2020/03/17 14:57:32 等待新的指令
我是3
2020/03/17 14:57:42 已发送: 我是3
2020/03/17 14:57:42 开始阻塞等待服务端返回数据
2020/03/17 14:57:42 已发送: 1584428252: 我是3
2020/03/17 14:57:42 等待新的指令
exit
2020/03/17 14:58:06 退出了

小结

利用go语言方便的协程切换来做多客户端监听,非常方便

golang-网络编程

程序幼儿员-龚学鹏
请先登录后发表评论
  • latest comments
  • 总共0条评论