用Golang创建一个并发的TCP客户端和服务器实例

839 阅读3分钟

你可以使用下面的简单例子来创建一个用Golang的并发TCP(传输控制协议)客户端和服务器的例子。拥有一个并发服务器的好处是你的服务器可以同时为多个客户端(连接)服务。TCP在设计上是一个可靠的通信协议。当TCP检测到数据包交付问题时,它要求重新传输丢失的数据,重新安排失序的数据,帮助最大限度地减少网络拥堵以减少其他问题的发生,等等。它在通过IP网络通信的主机上运行的应用程序之间使用。与其他网络协议一样,它也有自己的安全漏洞,这不在本文的讨论范围之内。与UDP(用户数据报协议)协议相比,TCP更可靠,但速度较慢。虽然这不在本篇文章的范围内,但我还是要给你一个关于UPD的非常简短的信息。UDP不提供错误检查、纠正或数据包重传,因此它不可靠但速度快。它通常用于视频会议、实时流媒体、在线游戏等实时应用。

客户端

package main

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

func main() {
	con, err := net.Dial("tcp", "0.0.0.0:9999")
	if err != nil {
		log.Fatalln(err)
	}
	defer con.Close()

	clientReader := bufio.NewReader(os.Stdin)
	serverReader := bufio.NewReader(con)

	for {
		// Waiting for the client request
		clientRequest, err := clientReader.ReadString('\n')

		switch err {
		case nil:
			clientRequest := strings.TrimSpace(clientRequest)
			if _, err = con.Write([]byte(clientRequest + "\n")); err != nil {
				log.Printf("failed to send the client request: %v\n", err)
			}
		case io.EOF:
			log.Println("client closed the connection")
			return
		default:
			log.Printf("client error: %v\n", err)
			return
		}

		// Waiting for the server response
		serverResponse, err := serverReader.ReadString('\n')

		switch err {
		case nil:
			log.Println(strings.TrimSpace(serverResponse))
		case io.EOF:
			log.Println("server closed the connection")
			return
		default:
			log.Printf("server error: %v\n", err)
			return
		}
	}
}

并行服务器

package main

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

func main() {
	listener, err := net.Listen("tcp", "0.0.0.0:9999")
	if err != nil {
		log.Fatalln(err)
	}
	defer listener.Close()

	for {
		con, err := listener.Accept()
		if err != nil {
			log.Println(err)
			continue
		}

		// If you want, you can increment a counter here and inject to handleClientRequest below as client identifier
		go handleClientRequest(con)
	}
}

func handleClientRequest(con net.Conn) {
	defer con.Close()

	clientReader := bufio.NewReader(con)

	for {
		// Waiting for the client request
		clientRequest, err := clientReader.ReadString('\n')

		switch err {
		case nil:
			clientRequest := strings.TrimSpace(clientRequest)
			if clientRequest == ":QUIT" {
				log.Println("client requested server to close the connection so closing")
				return
			} else {
				log.Println(clientRequest)
			}
		case io.EOF:
			log.Println("client closed the connection by terminating the process")
			return
		default:
			log.Printf("error: %v\n", err)
			return
		}

		// Responding to the client request
		if _, err = con.Write([]byte("GOT IT!\n")); err != nil {
			log.Printf("failed to respond to client: %v\n", err)
		}
	}
}

备注

客户端向服务器发送一个请求。服务器用GOT IT! 响应它。客户端可以通过输入:QUIT 并点击回车键来终止连接。服务器会理解这个请求并关闭客户端之间的相关连接。服务器也知道客户是否通过强制退出来终止连接,例如:Ctrl+C 终端信号或Ctrl+] 然后telnet> close 信号。如果你想使用Telnet ,而不是我们上面的客户端脚本进行测试,你可以如下图所示。客户端关闭连接并不意味着服务器就会关闭。除非明确关闭,否则它总是在运行。

TCP服务器将一直处于运行状态,并等待新的TCP客户端连接。每次有新的客户端连接到TCP服务器时,就会建立一个新的唯一的TCP连接。你可以通过运行netstat 命令来验证这一点,如下图所示:

# When we have two clients connected to the server. One with Telnet and another with our Go script.
$ netstat -anp TCP | grep 9999
tcp4       0      0  127.0.0.1.9999         127.0.0.1.51877        ESTABLISHED # -> Client 2: established conn
tcp4       0      0  127.0.0.1.51877        127.0.0.1.9999         ESTABLISHED # -> Server: listening Client 2 conn
tcp4       0      0  127.0.0.1.9999         127.0.0.1.51872        ESTABLISHED # -> Client 1: established conn
tcp4       0      0  127.0.0.1.51872        127.0.0.1.9999         ESTABLISHED # -> Server: listening Client 1 conn
tcp46      0      0  *.9999                 *.*                    LISTEN      # -> Server listening

# After clients close connections.
$ netstat -anp TCP | grep 9999
tcp46      0      0  *.9999                 *.*                    LISTEN 

有一点需要注意,连接会从ESTABLISHED 转到TIME_WAIT ,持续几秒钟,然后完全消失。TIME_WAIT 表示你/客户关闭了连接,因此连接保持开放,以便在实际关闭连接之前将任何未交付的数据包交付给连接进行处理。我们在一开始就提到了这一点。

测试

一旦你进入了一个客户会话,只要输入一些东西,看看会发生什么。你不妨也测试一下退出操作:

运行服务器

$ go run -race main.go

运行客户端

$ go run -race main.go

远程登录

$ telnet 0.0.0.0 9999