socket 函数是一个系统调用,用于在操作系统内核中创建一个新的网络套接字。套接字是一种在网络上进行通信的抽象对象,通过套接字,应用程序可以使用不同的网络协议进行通信,如 TCP、UDP 等,甚至我们可以实现自定义的协议。
syscall.Socket
可以很多介绍Go标准库的epoll方式的文章,但是介绍Go底层怎么创建TCP、UDP连接的文章还不是太多,Go最底层也是通过系统调用Socket的方式创建套接字,比如
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
| func sysSocket(family, sotype, proto int) (int, error) { syscall.ForkLock.RLock() s, err := socketFunc(family, sotype, proto) if err == nil { syscall.CloseOnExec(s) } syscall.ForkLock.RUnlock() if err != nil { return -1, os.NewSyscallError("socket", err) } if err = syscall.SetNonblock(s, true); err != nil { poll.CloseFunc(s) return -1, os.NewSyscallError("setnonblock", err) } return s, nil } var ( testHookDialChannel = func() {} testHookCanceledDial = func() {} socketFunc func(int, int, int) (int, error) = syscall.Socket connectFunc func(int, syscall.Sockaddr) error = syscall.Connect listenFunc func(int, int) error = syscall.Listen getsockoptIntFunc func(int, int, int) (int, error) = syscall.GetsockoptInt )
|
可以看到最终的还是依赖syscall.Socket
创建套接字的,最后返回这个套接字的文件描述符。
在 Linux 操作系统中,socket 函数的原型定义如下:
1 2 3
| #include <sys/socket.h> int socket(int domain, int type, int protocol);
|
它的主要功能就是创建一个网络交流的端点,返回一个文件描述符,通过这个文件描述符就可以进行数据的读写,甚至listen和accept。
该函数的参数含义如下:
- domain:指定套接字的地址族,例如 AF_INET 表示 IPv4 地址族,AF_INET6 表示 IPv6 地址族,AF_UNIX 表示本地 Unix 域套接字。
- type:指定套接字的类型,例如 SOCK_STREAM 表示面向连接的 TCP 套接字,SOCK_DGRAM 表示无连接的 UDP 套接字。
- protocol:指定套接字使用的协议,通常可以设置为 0,表示使用默认协议。
socket 函数的返回值为新创建的套接字的文件描述符,如果出现错误则返回 -1,并设置 errno 错误码。新创建的套接字可以使用其他系统调用进行配置,例如 bind
、connect
、listen
、accept
、sendto
、recvfrom
等等。这些系统调用在Go语言中都有相应的定义,Go包含两套系统,一个是标准库的syscall, 如syscall.Socket,一个是unix.Socket,如果你是在做Linux网络程序开发,其实都是一样的,所以本文中以syscall.Socket
库为例介绍。
domain
定义了套接字的地址族,比如我们常用的AF_INET
, AF_PACKET
代表数据链路层的raw socket。
type
类型常用的有SOCK_STREAM
、SOCK_DGRAM
、SOCK_RAW
等。
protocol
指常用的协议,根据前面的参数设置的不同,这个参数有不同的设置,比如第二个参数是SOCK_STREAM
、SOCK_DGRAM
时,我们常把protocol设置为0,当第二个参数是SOCK_RAW
时,我们会根据协议的不同,把它设置为syscall.IPPROTO_XXX
或者syscall.ETH_P_ALL
。
既然Go标准库中已经提供了TCP、UDP甚至IP的编程库,为什么我们还要学习syscall.Socket呢?syscall.Socket提供了底层的网络通讯方式,比标准库提供了更丰富的网络编程能力,新的协议,新的模式,而且能够提供更底层的网络控制能力。
在本文中,将使用大量的例子,演示syscall.Socket
不同的参数和不同的使用场景。
使用syscall.Socket实现一个最初级的HTTP Server
首先第一个例子是使用syscall.Socket
创建一个http server,这个http server纯粹是演示使用,并没有考虑tls、性能等方面,也没有考虑HTTP协议全特性支持,只是为了演示我们可以使用syscall.Socket
可以创建一个TCP Server。
一开始,我们先定义类型netSocket
,它包含Socket创建出来的文件描述符,我们把Read
、Write
、Accept
和Close
等系统调用都封装好,后面就方便使用了:
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
| type netSocket struct { fd int } func (ns netSocket) Read(p []byte) (int, error) { if len(p) == 0 { return 0, nil } n, err := syscall.Read(ns.fd, p) if err != nil { n = 0 } return n, err } func (ns netSocket) Write(p []byte) (int, error) { n, err := syscall.Write(ns.fd, p) if err != nil { n = 0 } return n, err } func (ns *netSocket) Accept() (*netSocket, error) { nfd, _, err := syscall.Accept(ns.fd) if err == nil { syscall.CloseOnExec(nfd) } if err != nil { return nil, err } return &netSocket{nfd}, nil } func (ns *netSocket) Close() error { return syscall.Close(ns.fd) }
|
以上代码也很容易理解,通过系统调用,操作socket文件描述符,我们就可以实现通用的服务端的网络处理能力。
接下来要解决的问题就是怎么生成一个Socket,让它监听指定的IP和端口:
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
| func newNetSocket(ip net.IP, port int) (*netSocket, error) { syscall.ForkLock.Lock() fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil { return nil, os.NewSyscallError("socket", err) } syscall.ForkLock.Unlock() if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { syscall.Close(fd) return nil, os.NewSyscallError("setsockopt", err) } sa := &syscall.SockaddrInet4{Port: port} copy(sa.Addr[:], ip) if err = syscall.Bind(fd, sa); err != nil { return nil, os.NewSyscallError("bind", err) } if err = syscall.Listen(fd, syscall.SOMAXCONN); err != nil { return nil, os.NewSyscallError("listen", err) } return &netSocket{fd: fd}, nil }
|
下面我们实现main函数,把真个创建、监听、接收连接、处理请求都串起来:
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 42 43
| func main() { ipFlag := flag.String("ip_addr", "127.0.0.1", "监听的地址") portFlag := flag.Int("port", 8080, "监听的端口") flag.Parse() ip := net.ParseIP(*ipFlag) socket, err := newNetSocket(ip, *portFlag) if err != nil { panic(err) } defer socket.Close() log.Printf("http addr: http://%s:%d", ip, port) for { rw, e := socket.Accept() log.Printf("incoming connection") if e != nil { panic(e) } log.Print("reading request") req, err := parseRequest(rw) log.Print("request: ", req) if err != nil { panic(err) } log.Print("writing response") io.WriteString(rw, "HTTP/1.1 200 OK\r\n"+ "Content-Type: text/html; charset=utf-8\r\n"+ "Content-Length: 20\r\n"+ "\r\n"+ "<h1>hello world</h1>") if err != nil { log.Print(err.Error()) continue } } }
|
这里有一个parseRequest
函数我还没有提供,它负责解析客户端的请求,解析出http request,实际这个;例子也不使用这个解析出的请求,只是简单的返回一个hello world的页面。这个函数实现如下:
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
| type request struct { method string header textproto.MIMEHeader body []byte uri string proto string } func parseRequest(c *netSocket) (*request, error) { b := bufio.NewReader(*c) tp := textproto.NewReader(b) req := new(request) var s string s, _ = tp.ReadLine() sp := strings.Split(s, " ") req.method, req.uri, req.proto = sp[0], sp[1], sp[2] mimeHeader, _ := tp.ReadMIMEHeader() req.header = mimeHeader if req.method == "GET" || req.method == "HEAD" { return req, nil } if len(req.header["Content-Length"]) == 0 { return nil, errors.New("no content length") } length, err := strconv.Atoi(req.header["Content-Length"][0]) if err != nil { return nil, err } body := make([]byte, length) if _, err = io.ReadFull(b, body); err != nil { return nil, err } req.body = body return req, nil }
|
你可以运行这个程序,然后在浏览器中访问 http://127.0.0.1:8080, 应该能看到hello world的页面。
通过这个程序,你可以看到使用syscall.Socket的方式也并不复杂,只需调用相应的系统调用就行,你知道了也就会了,使用的时候查一下文档或者例子就可以了。
使用syscall.Socket实现访问一个网页
上面的例子是使用syscall.Socket提供一个http server的例子,其实就是实现了一个tcp server。那么如果要实现一个访问网页的client,该如何使用syscall.Socket实现呢?
TCP socket需要调用系统调用Connect连接到服务器,连接后就可以通过Read/Write进行读写了:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| package main import ( "fmt" "net" "os" "syscall" ) func main() { sockfd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, 0) if err != nil { fmt.Println("socket creation failed:", err) os.Exit(1) } defer syscall.Close(sockfd) serverAddr, err := net.ResolveTCPAddr("tcp", "bing.com:80") if err != nil { fmt.Println("address resolution failed:", err) syscall.Close(sockfd) os.Exit(1) } err = syscall.Connect(sockfd, &syscall.SockaddrInet4{ Port: serverAddr.Port, Addr: [4]byte{serverAddr.IP[0], serverAddr.IP[1], serverAddr.IP[2], serverAddr.IP[3]}, }) if err != nil { fmt.Println("connection failed:", err) syscall.Close(sockfd) os.Exit(1) } request := "GET / HTTP/1.1\r\nHost: bing.com\r\n\r\n" _, err = syscall.Write(sockfd, []byte(request)) if err != nil { fmt.Println("write failed:", err) syscall.Close(sockfd) os.Exit(1) } response := make([]byte, 1024) n, err := syscall.Read(sockfd, response) if err != nil { fmt.Println("read failed:", err) syscall.Close(sockfd) os.Exit(1) } fmt.Println(string(response[:n])) }
|
这个例子中我们先创建了一个Socket,然后连接到bing.com网站,发送了一个简单的http请求并得到了返回结果。
使用syscall.Socket实现UDP server
上面的例子我们实现了TCP的Socket方式,接下来我们看看如何使用syscall.Socket实现UDP的server和client。
UDP的实现更简单,没有其他的Bind、Listen、Accept等系统调用。
注意Socket的三个参数的设置:syscall.AF_INET
、syscall.SOCK_DGRAM
和syscall.IPPROTO_UDP
。
UDP我们通过Sendto
和Recvfrom
进行读写:
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 42
| package main import ( "fmt" "net" "syscall" ) func main() { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) if err != nil { fmt.Println("socket failed:", err) return } defer syscall.Close(fd) addr := syscall.SockaddrInet4{Port: 8080} copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4()) if err := syscall.Bind(fd, &addr); err != nil { fmt.Println("bind failed:", err) return } fmt.Println("UDP server is listening on port 8080...") buf := make([]byte, 1024) for { n, from, err := syscall.Recvfrom(fd, buf, 0) if err != nil { fmt.Println("recvfrom failed:", err) continue } if err := syscall.Sendto(fd, buf[:n], 0, from); err != nil { fmt.Println("sendto failed:", err) } } }
|
这个例子我们实现了echo的功能,服务端收到什么,就把结果原封不动的返回给客户端。
使用syscall.Socket实现UDP client
既然使用syscall.Socket实现了UDP服务端,自然我们也能实现UDP的客户端:
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
| package main import ( "fmt" "net" "syscall" ) func main() { fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) if err != nil { fmt.Println("socket failed:", err) return } defer syscall.Close(fd) addr := syscall.SockaddrInet4{Port: 8080} copy(addr.Addr[:], net.ParseIP("127.0.0.1").To4()) message := "Hello, UDP server!" if err := syscall.Sendto(fd, []byte(message), 0, &addr); err != nil { fmt.Println("sendto failed:", err) return } buf := make([]byte, 1024) n, _, err := syscall.Recvfrom(fd, buf, 0) if err != nil { fmt.Println("recvfrom failed:", err) return } fmt.Println("received response:", string(buf[:n])) }
|
一开始也是创建一个UDP的socket, 然后调用Sendto和Recvfrom进行数据的发送和接收。可以看到我们不需要处理IP的包头和UDP的包头。
实现自定义的协议
前面的TCP和UDP的例子,我们调用syscall.Socket的时候,第一个参数是syscall.AF_INET
,后面根据TCP还是UDP分别设置数据流或者数据报的参数,这是IP层+UDP/TCP层的网络编程方式。
其实我们可以使用syscall.Socket实现数据链路层的通讯,比如接下来的例子,我们在数据链路层实现一个CHAO
协议。
CHAO
协议很简单,它的前四个字节分别是字母C
、H
、A
、O
,它没有IP的数据,纯粹建立在数据链路层之上,通过MAC Addr进行数据帧的发送和接收。
首先看一下服务端的实现:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| package main import ( "fmt" "net" "syscall" ) func main() { protocol := htons(syscall.ETH_P_ALL) fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(protocol)) if err != nil { fmt.Println("socket failed:", err) return } defer syscall.Close(fd) ifi, err := net.InterfaceByName("eth0") if err != nil { fmt.Println("interfaceByName failed:", err) return } addr := syscall.SockaddrLinklayer{ Protocol: protocol, Ifindex: ifi.Index, } if err := syscall.Bind(fd, &addr); err != nil { fmt.Println("bind failed:", err) return } buf := make([]byte, 1024) for { n, raddr, err := syscall.Recvfrom(fd, buf, 0) if err != nil { fmt.Println("recvfrom failed:", err) return } if n < 4 || !(buf[0] == 'C' && buf[1] == 'H' && buf[2] == 'A' && buf[3] == 'O') { continue } fmt.Printf("received request: %s\n", string(buf[4:n])) response := []byte("Hello, custom chao protocol!") header := []byte{'C', 'H', 'A', 'O'} packet := append(header, response...) if err := syscall.Sendto(fd, packet, 0, raddr); err != nil { fmt.Println("sendto failed:", err) return } } } func htons(i uint16) uint16 { return (i<<8)&0xff00 | i>>8 }
|
首先我们使用syscall.AF_PACKET
、 syscall.SOCK_RAW
, syscall.ETH_P_ALL
创建了一个raw socket,注意它和我们前面TCP/UDP例子不同,TCP/UDP创建socket的时候第一个参数是IPv4的地址族,创建数据链路层的raw socket,我们使用的是syscall.AF_PACKET
,而且第二个参数是syscall.SOCK_RAW
,第三个参数是syscall.ETH_P_ALL
,代表接收所有的协议的数据。
接下来将socket绑定到某个网卡上。我在测试的时候,发现syscall.SockaddrLinklayer需要通过htons把协议从主机字节序转换成网络字节序,但是调用syscall.Socket
的时候第三个参数转不转换好像都没有问题。
接下来就是调用Recvfrom
读取客户端的请求,这里检查如果不是CHAO协议的数据就忽略,如果是CHAO协议的数据,就把请求原封不动的写回回去。
上面是服务端的代码,接下来是客户端的代码:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| package main import ( "fmt" "net" "syscall" ) func main() { protocol := htons(syscall.ETH_P_ALL) fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(protocol)) if err != nil { fmt.Println("socket failed:", err) return } defer syscall.Close(fd) ifi, err := net.InterfaceByName("eth0") if err != nil { fmt.Println("interfaceByName failed:", err) return } addr := syscall.SockaddrLinklayer{ Protocol: protocol, Halen: 6, Ifindex: ifi.Index, } copy(addr.Addr[:6], ifi.HardwareAddr) if err := syscall.Bind(fd, &addr); err != nil { fmt.Println("bind failed:", err) return } message := []byte("Hello, custom chao protocol!") header := []byte{'C', 'H', 'A', 'O'} packet := append(header, message...) if err := syscall.Sendto(fd, packet, 0, &addr); err != nil { fmt.Println("sendto failed:", err) return } buf := make([]byte, 1024) for { n, _, err := syscall.Recvfrom(fd, buf, 0) if err != nil { fmt.Println("recvfrom failed:", err) return } if n < 4 || !(buf[0] == 'C' && buf[1] == 'H' && buf[2] == 'A' && buf[3] == 'O') { continue } fmt.Println("received response:", string(buf[4:n])) break } } func htons(i uint16) uint16 { return (i<<8)&0xff00 | i>>8 }
|
和服务端的代码类似,也是创建Socket并绑定,这里我们是在同一台机器上进行测试的,所以绑定的地址都是本机的eth0网卡的地址。客户端先发送一个数据,然后等待服务端的返回。
其他
因为syscall.Socket是非常底层的网络编程的方式,你可以使用它做很多标准库没有办法提供的功能。比如你可以实现arp、dhcp、icmp等协议,限于文章的篇幅,我就不一一列举了,感兴趣的同学可以在原文的注释中讨论。
参考文档
- https://man7.org/linux/man-pages/man2/socket.2.html
- https://www.halolinux.us/kernel-reference/the-socket-system-call.html
- https://gist.github.com/jschaf/93f37aedb5327c54cb356b2f1f0427e3