Go Book / 1 Go Basics / 18 Go网络编程(一): Socket编程

18 Go网络编程(一): Socket编程

一、Socket编程简述

什么是Socket?

Socket,中文翻译为“套接字”,初学者听到这名词第一反应是蒙圈。所以“套接字”是什么鬼?你可以把它理解为“插座”。在通信领域,计算机之间的通信需要知道对方地址,开哪个“门”?用什么协议?实现这种通信连接完成数据交互的技术叫Socket通信技术。在计算机历史发展中,有许多方式实现Socket通信,如unix socket、Unix domain socket、X.25 socket、Internet socket,其中最常用的是Internet socket,这种socket编程基于网络层和传输层协议,即TCP/IP协议族。网络层和传输层属于计算机多层网络模型(七层/五层/四层模型),这些基础这里就不展开了,出门右转百度或谷歌一下。

怎么实现Socket编程?

了解完Socket,我们知道最常用的Internet Socket是一种基于TCP/IP协议族的通信方式。这种网络Socket分两种实现,一种是基于安全连接的TCP协议实现的TCP Socket,另一种是基于非安全连接的UDP协议实现的UDP Socket,其适用场景不同。

TCP

TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。这里提到可靠传输,要知道网络环境是不可信的,现实中存在许多网络传输丢包的情况,在这种情况下要实现可靠传输需要通信双方多次确认连接状态,这就靠TCP连接中的“三次握手四次挥手”进行可靠确认了。

UDP

UDP 是User Datagram Protocol的简称, 中文名是用户数据报协议。它是一个无连接的协议,其数据传输是不可靠的,通俗的讲这种通信方式就是一端把数据包写上对方地址往外一扔完事,在通信过程中是否丢包它不关注,但也因为如此它通信效率比TCP高

这里提一些TCP UDP协议的简单概念,具体通信原理这里不再展开。

二、Go Socket编程

了解完Socket编程的简要概念,我们来关注Socket编程在Go中的实现。网络Socket是一种典型的C/S结构,即有服务端启动的守护进程接收数据,也有客户端发起请求。所以具体实现就是编写一对通信端。Go提供net网络包实现基本的tcp/udp网络编程能力。使用net包非常简单。

1.TCP Socket

服务端实现思路:
  • 首先启动一个监听器,声明传输协议与主机端口信息;
  • 启动一个无限循环不断从监听器接收客户端连接;
  • 为每个客户端连接创建一个处理协程,处理协程实现对客户端的响应。
客户端实现思路:
  • 首先客户端拨号连接服务端,声明传输协议与服务端主机和端口信息;
  • 准备一个读写缓冲的字节数组
  • 启动一个无限循环不断从终端读取用户输入,通过缓冲字节数组发送给服务端并等待服务端响应。

编写一个TCP 服务端:tcp_server.go

//DONE:
//- 服务端在本机的任一端口建立TCP监听
//- 为接入的每一个客户端开辟一条独立的协程
//- 循环接收客户端消息,不管客户端说什么,都自动回复“已阅xxx”
//- 如果客户端说的是“im off”,则回复“bye”并断开其连接;
//- 连接处理器不断接收客户端信息,知道客户端申请断开连接,服务端把连接关闭。
func main() {
	//启动监听器
	listener, err := net.Listen("tcp", "localhost:12138")
	defer listener.Close()
	SErrorFunc(err, "net.Listen")

	//tcp服务端应不断处于等待连接状态
	for {
		//从监听器不断接收连接
		conn, err := listener.Accept() //阻塞,直到建立连接
		defer conn.Close()
		SErrorFunc(err, "listener.Accept")

		//每个连接接收到请求马上开启子协程进行对话
		go ChatWith(conn)
	}

}

func ChatWith(conn net.Conn) {
	//开辟一个接收桶接收信息
	bytes := make([]byte, 1024)

	//每个连接不断产生会话,直到客户端断开连接
	for {
		//不断读取来自客户端的信息
		n, err := conn.Read(bytes)
		SErrorFunc(err, "conn.Read")
		clientMsg := string(bytes[:n])
		fmt.Printf("收到客户端%s发来的消息:%s\n", conn.RemoteAddr(), clientMsg) //读取出来打印

		//给客户端写信息
		if clientMsg != "bye" {
			conn.Write([]byte("已阅" + clientMsg))
		} else {
			conn.Write([]byte("Fuck off!"))
			break //客户端 bye后退出对话循环
		}
	}

	//断开与客户端的连接
	conn.Close()
	fmt.Printf("客户端%s断开连接\n", conn.RemoteAddr())
}

//服务端错误处理函数
func SErrorFunc(err error, when string) {
	if err != nil {
		fmt.Println("服务端错误,此处报错:", err, when)
		os.Exit(1)
	}
}

编写一个TCP 客户端:tcp_client.go

//DONE:
//- 拨号连接服务端主机的端口,建立连接
//- 循环从标准输入(命令行)读取一行用户输入,向服务端发送
//- 接收并打印服务端消息,如果消息是“bye”,就退出程序
func main() {
	//客户端拨号连接服务端
	conn, err := net.Dial("tcp", "127.0.0.1:12138")
	defer conn.Close()
	CErrorFunc(err, "net.Dial")

	//创建一个承载信息的桶
	bytes := make([]byte, 1024)

	//准备一个终端输入读取器
	reader := bufio.NewReader(os.Stdin)
	for {
		//读取器不断从终端接收读取,每收到一个终端信息就发送给服务器
		lineByte, _, err := reader.ReadLine()
		CErrorFunc(err, "reader.Read")
		conn.Write(lineByte)

		//接收服务端的返回信息
		n, err := conn.Read(bytes)
		CErrorFunc(err, "conn.Read")
		serverMsg := string([]byte(bytes[:n]))
		if string(lineByte) != "bye" {
			fmt.Println("服务端信息:", serverMsg)
		} else {
			fmt.Println("客户端程序结束,GAME OVER!")
			os.Exit(0)

		}

	}

}

//客户端的错误处理函数
func CErrorFunc(err error, when string) {
	if err != nil {
		fmt.Println("客户端错误,此处报错:", err, when)
		os.Exit(1)
	}
}

实现了TCP Socket的Server端和Client端后,分别用go build编译成server和client两个可执行文件,各自在独立的终端启动后就可实现基于TCP的socket通信了。

2.UDPSocket

服务端实现思路:

UDP服务相当于在服务端开一个大的监听桶,客户端的连接信息都不断的发送到桶里,再由udp服务端处理。

  • 首先解析一个UDP协议的服务地址,声明传输协议与主机端口信息;
  • 启动一个UDP监听器,传入传输协议和解析的服务地址;
  • 准备一个读写缓冲的字节数组
  • 启动一个无限循环不断从监听器接收客户端信息并处理;
客户端实现思路:

与TCP客户端类似,把传输协议换成UDP即可。

  • 首先客户端拨号连接服务端,声明传输协议与服务端主机和端口信息;
  • 准备一个读写缓冲的字节数组
  • 启动一个无限循环不断从终端读取用户输入,通过缓冲字节数组发送给服务端并等待服务端响应。

编写一个UDP服务端:udp_server.go

//DONE:
//- 服务端在本机的任一端口建立UDP监听,得到广口连接
//- 循环接收客户端消息,不管客户端说什么,都自动回复“已阅xxx”
//- 如果客户端说的是“im off”,则回复“bye”
func main() {
	//先解析得到UDP地址
	udpAddr, err := net.ResolveUDPAddr("udp", "localhost:12139")
	//fmt.Println(udpAddr)
	SErrorFunc(err, "net.ResolveUDPAddr")
	//把解析得到的udp地址建立监听,UDP只有一个广口连接,接收一切信息,信息来源地址在消息包里
	conn, err := net.ListenUDP("udp", udpAddr)
	defer conn.Close()
	SErrorFunc(err, "net.ListenUDP")

	//创建消息缓冲区
	bytes := make([]byte, 1024)

	for {
		//udp连接不断的接收信息
		n, remoteAddr, err := conn.ReadFromUDP(bytes)
		SErrorFunc(err, "conn.ReadFromUDP")

		//打印数据包内容
		clientMsg := string(bytes[:n])
		fmt.Printf("来自客户端%s的消息:%s\n", remoteAddr, clientMsg)

		if clientMsg != "bye" {
			conn.WriteToUDP([]byte("已阅:"+clientMsg), remoteAddr)
		} else {
			conn.WriteToUDP([]byte("off"), remoteAddr)
		}
	}
}

编写一个UDP客户端:udp_client.go

//DONE:
//- 拨号连接服务端主机的端口,建立UDP连接
//- 循环从标准输入(命令行)读取一行用户输入,向服务端发送
//- 接收并打印服务端消息,如果消息是“bye”,就退出程序
func main() {

	//拨号UDP连接
	conn, err := net.Dial("udp", "127.0.0.1:12139")
	defer conn.Close()
	CErrorFunc(err, "net.Dial")

	//创建一个标准输入
	reader := bufio.NewReader(os.Stdin)
	bytes := make([]byte, 1024)
	for {
		//终端标准输入发送给服务端
		readLine, _, err := reader.ReadLine()
		CErrorFunc(err, "reader.ReadLine")
		conn.Write(readLine)

		//从服务端接收信息
		n, err := conn.Read(bytes)
		CErrorFunc(err, "conn.Read")

		//服务端信息
		serverMsg := string(bytes[:n])
		fmt.Printf("服务短信息:%s\n", serverMsg)

		if serverMsg == "off" {
			fmt.Println("服务端已断开!")
			break
		}
	}

	fmt.Println("GAME OVER!!!")
	os.Exit(0)

}