【Go网络编程】socket编程

168 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 12 天,点击查看活动详情

socket

"Socket" 是传输层给用户提供的编程接口,提供了在计算机之间发送数据的方法。它允许程序员在两台计算机之间创建网络连接,并在这两台计算机之间进行数据交换。

因此socket与tcp、udp等的关系就明确了,socket是一种编程方式,tcp、udp、unix的pipeline是具体的数据交换协议,socket编程方式可以用这些协议来实现。下面具体对tcp和udp的socket编程进行介绍。

网络 I/O 模型

在介绍socket编程之前,先科普一下基本的网络IO模型。网络 I/O 模型定义的是应用线程操作系统内核之间的交互行为模式。

  • 阻塞:操作系统等数据全部就绪后,才返回
  • 非阻塞:内核查看数据就绪状态后,即便没有就绪也立即返回错误

阻塞 I/O(Blocking I/O)

image.png

特点:一个线程仅能处理一个网络连接上的数据通信。即便连接上没有数据,线程也只能阻塞在对 Socket 的读操作上。各大平台在默认情况下都将 Socket 设置为阻塞的。

非阻塞 I/O(Non-Blocking I/O)

image.png

请求发起者通常会通过轮询的方式,去一次次发起 I/O 请求,直到读到所需的数据为止。cpu性能浪费,很少使用。

I/O 多路复用(I/O Multiplexing)

避免阻塞的低效和轮询的资源浪费,使用基于内核提供的多路复用(select/poll、epoll)函数的 I/O 多路复用模型。 image.png 应用线程首先将需要进行 I/O 操作的 Socket,添加到多路复用函数中(以 select 为例),然后阻塞,等待 select 系统调用返回。当内核发现有数据到达时,对应的 Socket 具备了通信条件, select 函数返回。然后用户线程会针对这个 Socket 再次发起网络 I/O 请求,比如一个 read 操作。由于数据已就绪,这次网络 I/O 操作将得到预期的操作结果。一个应用线程可以同时处理多个 Socket,同时,I/O 多路复用模型由内核实现可读 / 可写事件的通知。

go语言io模型

看似是阻塞的,其实是复用的,通过网络轮询器(netpoller)只阻塞执行网络 I/O 操作的 Goroutine,但不阻塞执行 Goroutine 的线程(也就是 M)。

当用户层针对某个 Socket 描述符发起read操作时,如果这个 Socket 对应的连接上还没有数据,运行时就会将这个 Socket 描述符加入到 netpoller 中监听,同时发起此次读操作的 Goroutine 会被挂起。直到 Go 运行时收到这个 Socket 数据可读的通知,Go 运行时才会重新唤醒等待在这个 Socket 上准备读数据的那个 Goroutine。而这个过程,从 Goroutine 的视角来看,就像是 read 操作一直阻塞在那个 Socket 描述符上一样。

netpoller采用了 I/O 多路复用的模型,在不同操作系统上,使用操作系统各自实现的高性能多路复用函数,比如:Linux 上的 epoll、Windows 上的 iocp、FreeBSD/MacOS 上的 kqueue、Solaris 上的 event port 等,这样可以最大程度提高 netpoller 的调度和执行性能。

Socket 编程

image.png

基于tcp

服务端代码:

package main

import (
	"bufio"
	"fmt"
	"net"
)

func main() {
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("Error listening:", err)
		return
	}
	defer ln.Close()

	fmt.Println("Listening on port 8080...")
	for {
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("Error accepting connection:", err)
			continue
		}

		go handleConnection(conn)
	}
}

func handleConnection(conn net.Conn) {
	defer conn.Close()

	fmt.Println("Received a connection from", conn.RemoteAddr().String())
	scanner := bufio.NewScanner(conn)
	for scanner.Scan() {
		message := scanner.Text()
		fmt.Println("Received message:", message)
	}
}

客户端代码:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting:", err)
		os.Exit(1)
	}
	defer conn.Close()

	fmt.Println("Connected to server. Type a message and press enter:")
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		message := scanner.Text()
		if message == "exit" {
			break
		}
		_, err := fmt.Fprintf(conn, message+"\n")
		if err != nil {
			fmt.Println("Error sending message:", err)
			break
		}
		fmt.Println("Message sent:", message)
	}
}

基于udp

服务端:

package main

import (
	"fmt"
	"net"
)

func main() {
	addr, err := net.ResolveUDPAddr("udp", ":8080")
	if err != nil {
		fmt.Println("Error resolving UDP address:", err)
		return
	}

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		fmt.Println("Error listening on UDP:", err)
		return
	}
	defer conn.Close()

	fmt.Println("Listening on port 8080...")
	var buf [1024]byte
	for {
		n, src, err := conn.ReadFromUDP(buf[:])
		if err != nil {
			fmt.Println("Error reading from UDP:", err)
			continue
		}

		fmt.Printf("Received %d bytes from %s: %s\n", n, src, string(buf[:n]))
	}
}

客户端:

package main

import (
	"fmt"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("udp", "localhost:8080")
	if err != nil {
		fmt.Println("Error connecting:", err)
		os.Exit(1)
	}
	defer conn.Close()

	fmt.Println("Connected to server. Type a message and press enter:")
	var message string
	for {
		fmt.Scanln(&message)
		if message == "exit" {
			break
		}
		_, err := fmt.Fprintf(conn, message+"\n")
		if err != nil {
			fmt.Println("Error sending message:", err)
			break
		}
		fmt.Println("Message sent:", message)
	}
}

基于unix本地套接字

package main

import (
	"fmt"
	"net"
	"os"
)

const (
	SOCKET_FILE = "/tmp/go_socket"
)

func checkError(err error) {
	if err != nil {
		fmt.Println("Error:", err)
		os.Exit(1)
	}
}

func server() {
	// 删除套接字文件
	os.Remove(SOCKET_FILE)

	// 监听 Unix 域套接字
	listener, err := net.Listen("unix", SOCKET_FILE)
	checkError(err)
	defer listener.Close()
	fmt.Println("Server is running")

	// 接受客户端请求
	for {
		conn, err := listener.Accept()
		checkError(err)
		go handleRequest(conn)
	}
}

func handleRequest(conn net.Conn) {
	defer conn.Close()
	buf := make([]byte, 1024)
	for {
		// 读取客户端数据
		n, err := conn.Read(buf)
		if err != nil {
			return
		}
		fmt.Println("Received from client:", string(buf[:n]))

		// 向客户端发送数据
		_, err = conn.Write([]byte("Hello, client"))
		if err != nil {
			return
		}
	}
}

func client() {
	// 连接 Unix 域套接字
	conn, err := net.Dial("unix", SOCKET_FILE)
	checkError(err)
	defer conn.Close()

	// 向服务端发送数据
	_, err = conn.Write([]byte("Hello, server"))
	checkError(err)

	// 读取服务端数据
	buf := make([]byte, 1024)
	n, err := conn.Read(buf)
	checkError(err)
	fmt.Println("Received from server:", string(buf[:n]))
}

func main() {
	if len(os.Args) > 1 && os.Args[1] == "server" {
		server()
	} else {
		client()
	}
}