首页 > 编程笔记 > Go语言笔记 阅读:14

Go语言Socket编程(基于TCP,附带实例)

Socket 起源于 UNIX,在 UNIX 系统中,Socket 作为一种特殊的文件描述符,用于实现网络通信。

UNIX 的基本哲学是一切皆文件。在 UNIX 中,所有的文件都可以基于打开→读写→关闭模式进行操作,而 Socket 就是这种模式的一种实现。

使用 Socket() 函数创建一个新的 Socket,它会返回一个整型的 Socket,这个描述符在后续的建立连接、数据传输等操作中都会被使用,使用方式类似于其他文件描述符。

Socket 通信需要用到两个要素,它们分别是网络协议和地址:
下面来看看基于 TCP 协议的 Socket 编程的实现方式。在基于 TCP 协议的网络通信中,服务端每次与客户端通信都必须建立握手。

建立握手的示意图如下图所示:


图 1 基于TCP协议建立握手的示意图

Go语言Socket通信的过程

为了更深入地理解基于 TCP 协议的 Socket 通信,接下来我们详细分析服务端和客户端是如何利用 Go 语言实现这一过程的。

1) 服务端

要使用 Go 语言实现基于 TCP 协议的 Socket 通信,服务端需要执行以下步骤:
① 导入 net 包,它提供了与网络相关的基本功能。

② 使用 net.Listen 方法创建监听器(Listener)。具体操作为先绑定并监听端口,然后等待客户端与其建立连接。关键代码如下:
//创建监听器
listener, err := net.Listen("tcp", "127.0.0.1:8000")
if err != nil {
     fmt.Printf("net.Listen() err:%v\n", err)
     return
}

③ 接受客户端连接。net.Listen 的 listener.Accept 方法用于监听并接受来自客户端的连接。如果客户端尚未建立连接,该方法会阻塞等待。当客户端连接成功时,Accept 方法返回一个用于与客户端通信的新连接对象(net.Conn 接口类型的对象),开发者可以通过该连接对象进行数据的读、写操作。

如果在等待新连接的过程中发生了错误(例如监听器被关闭),listener.Accept 方法会返回一个非 nil 的错误。想要持续接受客户端的连接,可以使用 for 循环持续地调用 Accept 方法。关键代码如下:
for {
        fmt.Println("服务器等待客户端连接...")
        conn, err := listener.Accept()
        if err != nil {
             fmt.Printf("listener.Accept() err:%v\n", err)
             return
        }
        …
}

④ 创建协程处理客户端请求。为了实现并发地处理多个客户端请求,服务端通常会针对每个连接创建一个协程。

⑤ 使用新连接(Socket)进行通信。此过程通常使用 conn.Read 和 conn.Write 方法实现。关键代码如下:
//注意,conn.Read方法在执行读取操作时,会将命令行里的换行符也给读取了
//在UNIX上换行符是\n,在Windows上是\r\n
n, err := conn.Read(buf)
if err != nil {
        if err == io.EOF {
             fmt.Println("客户端退出!")
             break
        } else {
             fmt.Printf("conn.Read() err:%v\n", err)
             return
        }
}
fmt.Printf("服务器读到数据:%v", string(buf[:n]))
//小写转大写,发回给客户端
conn.Write(bytes.ToUpper(buf[:n]))

⑥ 处理客户端请求。根据客户端发送的数据执行相应的操作,并返回结果给客户端。

⑦ 关闭连接。在完成与客户端的通信后,使用 conn.Close 方法关闭 Socket 连接。

⑧ 关闭监听器。如果不再接受新的客户端连接,使用 listener.Close 方法关闭监听器。

2) 客户端

要使用 Go语言实现基于 TCP 协议的 Socket 通信,客户端需要执行以下步骤:
① 导入 net 包,它提供了与网络相关的基本功能。

② 建立连接。使用 net.Dial 函数连接到服务端,指定使用的协议(如 TCP 协议)和服务端的地址、端口。如果连接成功,该函数将返回一个用于与服务端通信的 ocket 连接。关键代码如下:
//发起连接请求
conn, err := net.Dial("tcp", "127.0.0.1:8000")
if err != nil {
        fmt.Printf("net.Dial() err:%v\n", err)
        return
}

③ 使用连接进行通信。通过 Socket 连接向服务端发送数据,这里通常会使用 conn.Write 函数实现相应的功能。数据可以是字节切片或字符串等。关键代码如下:
//获取用户的键盘输入(os.Stdin),并将输入的数据发送给服务器
go func() {
        str := make([]byte, 1024)
        for {
             n, err := os.Stdin.Read(str)
             if err != nil {
                     fmt.Printf("os.Stdin.Read() err:%v\n", err)
                     continue
             }
             //将数据发送给服务器
             conn.Write(str[:n])
        }
}()

④ 读取和处理服务端返回的数据。先通过 Socket 连接读取服务端返回的数据,使用 conn.Read 函数读取数据并存储在一个字节切片中,然后解析服务端返回的数据,并执行相应的操作。关键代码如下:
for {
        buf := make([]byte, 1024)
        n, err := conn.Read(buf)
        if err != nil {
             if err == io.EOF {
                     fmt.Println("服务端退出了!!!")
                     return
             } else {
                     fmt.Printf("conn.Read() err:%v\n", err)
                     continue
             }
        }
        fmt.Printf("客户端读到服务器返回的数据:%s",buf[:n])
}

⑤ 关闭连接。在完成与服务端的通信后,使用 conn.Close 函数关闭 Socket 连接。

服务端与客户端的通信是双向的。客户端需要不断地向服务端发送数据,同时也要接收来自服务端数据。因此,发送和接收操作应分别放在不同的协程中进行。主协程负责循环接收服务器返回的数据并输出,子协程则负责循环读取用户从键盘输入的数据,并将其发送给服务端。

在读取键盘输入数据的子协程中定义一个切片 str,使用 os.Stdin.Read(str) 函数将读取到的数据保存起来。这样,客户端也实现了并发多任务处理。

在基于 UDP 协议的并发 Socket 编程中,由于 UDP 协议没有握手,因此服务器端无须额外创建监听套接字,只需要指定 IP 地址和端口,然后监听该地址,等待客户端发起连接即可。一旦建立连接,便可以进行通信。

相关文章