1. 前言

​ 最近工作当中用 Python 写了非常多的 socket 代码,用于和底层的设备之间进行交互。然而我的方式比较原始,自己在一个基础的 socket 上不断地进行扩展。总所周知,Python 的网络编程界有一个大名鼎鼎的Twisted框架,Twisted 是已经一个维护了十余年的成熟项目,基于事件驱动设计的高性能网络编程框架。奈何这个框架的学习成本比较高,再由于笔者最近在学习 Go 语言,所以想着不如在 Go 语言中折腾一下网络编程,以下就是笔者学习阶段的一些总结。

2. Socket 编程起源

socket 起源于 Unix,而 Unix/Linux 基本哲学之一就是 “一切皆文件”,都可以用 “打开 open –> 读写 write/read –> 关闭 close” 模式 来操作。Socket 就是该模式的一个实现,socket 即是一种特殊的文件,一些 socket 函数就是对其进行的操作(读/写 IO、打开、关闭)

listenacceptwriteread

EXEMo8.png

3. 再回首——Python 中的 socket

在进入到 Go 语言的世界前,和我再来回顾一下,Python 中 Socket 编程的基础实例:

Server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

import socket

HOST = '' # Symbolic name meaning all available interfaces
PORT = 50007 # Arbitrary non-privileged port

sock_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_server.bind((HOST, PORT))

sock_server.listen(1) #开始监听,1代表在允许有一个连接排队,更多的新连接连进来时就会被拒绝
conn, addr = sock_server.accept() #阻塞直到有连接为止,有了一个新连接进来后,就会为这个请求生成一个连接对象

with conn:
print('Connected by', addr)
while True:
data = conn.recv(1024) #接收1024个字节
if not data: break #收不到数据,就break
conn.sendall(data) #把收到的数据再全部返回给客户端

Client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
# Echo client program
import socket

HOST = 'localhost' # The remote host
PORT = 50007 # The same port as used by the server

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((HOST, PORT))
client.sendall(b'Hello, world')

data = client.recv(1024)

print('Received',data)

显而易见,Python 的代码哲学还是有优点的。好了,整理好心情,和 Python 道声再见,我们进入 Go 的世界。

4. Go SOCKET 基础概念

4.1 IP 类型

net 包中定义的 IP 类型直接就是 byte 数组:

1
type IP []byte
func parseIP(s string) IP
1
2
3
4
5
6
7
ipAddr := "192.168.1.79"
addr := net.ParseIP(ipAddr)
if addr == nil{
fmt.Println("unavaliable addr")
}else{
fmt.Println(addr.To16())
}

4.2 函数

4.2.1 funcResolveTCPAddr(net, addr string) (*TCPAddr, error)

函数的功能是解析TCP连接的地址,包含和
nettcptcp4tcp6tcp4addr[ip+port][domain+port]
*TCPAddr
1
2
3
4
5
type TCPAddr struct {
IP IP
Port int
Zone string // IPv6 scoped addressing zone
}

4.2.2 func ResolveIPAddr(net, addr string) (*IPAddr, error)

ResolveIPAddr
netipip4ip6ip4addr
*IPAddr
1
2
3
4
type IPAddr struct {
IP IP
Zone string // IPv6 scoped addressing zone
}

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

Dial
networktcptcp4tcp6;如果是IP连接,对应ip ip4 ip6addressip+portdomain+port217.0.0.1
net.ConnWrite()Read()
1
2
3
4
5
6
7
8
9
10
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}

与这个函数相对应的两个函数:

func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
laddrnil

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

conn[]byte
1
2
3
4
5
n, err := tcpCoon.Write([]byte("HelloWorld"))
if err != nil{
fmt.Println(err)
return
}

4.2.5 func (c *conn) Read(b [] byte) (int, error)

从 conn 连接对象中读取数据,成功将返回读取到的字节数。

1
2
3
4
5
6
7
recvData := make([]byte, 2048)
n, err = tcpCoon.Read(recvData)
if err != nil{
fmt.Println(err)
return
}
fmt.Println(string(recvData))

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

Listen 函数在服务端使用,让服务端开始监听。

nettcpipladdrip+port127.0.0.1

相应的两个函数:

func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)

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

Accept

相应的还有一个

func (l *TCPListener) AcceptTCP() (*TCPConn, error)

5. Go 基础 scoket 代码实例

经过上面的介绍,相信大家对 Go 的 socket 编程已经有了一些了解。现在我们就来写一个 server 和 client,实现功能:client 发送数据到 server,server 将数据转成大写后返回。

server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"net"
"fmt"
"strings"
)

func (){
tcpAddr, err := net.ResolveTCPAddr("tcp4", "localhost:8080") //创建一个TCPAddr
if err != nil{
fmt.Println(err)
return
}

tcpLinstener, err := net.ListenTCP("tcp4", tcpAddr) //开始监听
if err != nil{
fmt.Println(err)
return
}
fmt.Printf("Start listen:[%s]
", tcpAddr)

tcpCoon, err := tcpLinstener.AcceptTCP() //阻塞,等待客户端连接
if err != nil{
fmt.Println(err)
return
}
defer tcpCoon.Close() //记得关闭连接对象

data := make([]byte, 2048)
n, err := tcpCoon.Read(data) //客户端连接后,开始读取数据
if err != nil{
fmt.Println(err)
return
}

recvStr := string(data[:n])
fmt.Println("Recv:", recvStr)
tcpCoon.Write([]byte(strings.ToUpper(recvStr))) //转换成大写后返回客户端
}

client.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
"net"
"fmt"
)

func (){
tcpAddr, err := net.ResolveTCPAddr("tcp4", "localhost:8080") //TCP连接地址
if err != nil{
fmt.Println(err)
return
}

tcpCoon, err := net.DialTCP("tcp4", nil, tcpAddr) //建立连接
if err != nil{
fmt.Println(err)
return
}
defer tcpCoon.Close() //关闭

sendData := "helloworld"
n, err := tcpCoon.Write([]byte(sendData)) //发送数据
if err != nil{
fmt.Println(err)
return
}
fmt.Printf("Send %d byte data success: %s
", n, sendData)

recvData := make([]byte, 2048)
n, err = tcpCoon.Read(recvData) //读取数据
if err != nil{
fmt.Println(err)
return
}
recvStr := string(recvData[:n])
fmt.Printf("Response data: %s", recvStr)

6. 小结

至此,相信大家对 Go 的网络编程已经有了一个大致的了解。

后续的计划是

  • Go 并发 socket 编程
  • Go 的 socket 编程粘包处理
  • 挑选一个成熟的 Go 网络编程框架进行学习

参考资料