GO:Socket编程

目录

一、TCP/IP协议族和四层模型概述

[1.1 互联网协议族(TCP/IP)](#1.1 互联网协议族(TCP/IP))

[1.2 TCP/IP四层模型](#1.2 TCP/IP四层模型)

[1. 网络访问层(Network Access Layer)](#1. 网络访问层(Network Access Layer))

[2. 网络层(Internet Layer)](#2. 网络层(Internet Layer))

[3. 传输层(Transport Layer)](#3. 传输层(Transport Layer))

[4. 应用层(Application Layer)](#4. 应用层(Application Layer))

[1.3 特点和作用](#1.3 特点和作用)

二、Socket基础

[2.1 Socket简介](#2.1 Socket简介)

[2.2 TCP编程](#2.2 TCP编程)

[2.2.1 TCP简介](#2.2.1 TCP简介)

[2.2.2 TCP的关键特性](#2.2.2 TCP的关键特性)

[2.2.3 TCP客户端](#2.2.3 TCP客户端)

[2.2.4 TCP服务端](#2.2.4 TCP服务端)

[2.3 UDP编程](#2.3 UDP编程)

[2.3.1 UDP简介](#2.3.1 UDP简介)

[2.3.2 UDP客户端](#2.3.2 UDP客户端)

[2.3.3 UDP服务端](#2.3.3 UDP服务端)


在学习 Socket之前,我们需要了解、什么是TCP/IP以及如何使用TCP/IP中的Socket 连接实现网络通信。Socket是我们在使用Go语言的过程中会使用到的最底层的网络协议,大部分的网络通信协议都是基于TCP/IP的Socket协议

一、TCP/IP协议族和四层模型概述

1.1 互联网协议族(TCP/IP)

  • 是互联网的基础通信架构
  • 包含整个网络传输协议家族
  • 核心协议:TCP(传输控制协议)和IP(网际互连协议)
  • 提供点对点的链接机制,标准化数据封装、定址、传输、路由和接收

1.2 TCP/IP四层模型

1. 网络访问层(Network Access Layer)

  • 未详细描述,指出主机必须使用某种协议与网络相连

2. 网络层(Internet Layer)

  • 关键部分,使用IP协议
  • 功能:使主机可以发送分组到任何网络
  • 特点:分组可能经由不同网络,到达顺序可能不同

3. 传输层(Transport Layer)

  • 定义两个端到端协议:TCP和UDP
  • TCP:面向连接,提供可靠传输、流量控制、多路复用等
  • UDP:无连接,不可靠传输,用于简单应用

4. 应用层(Application Layer)

  • 包含所有高层协议
  • 主要协议:
    • TELNET(远程终端)
    • FTP(文件传输)
    • SMTP(电子邮件)
    • DNS(域名服务)
    • HTTP(超文本传输)
    • ...

1.3 特点和作用

  • 将软件通信过程抽象为四个层
  • 采用协议堆栈方式实现不同通信协议
  • 简化了OSI七层模型
  • 提供了灵活、可扩展的网络通信框架

二、Socket基础

网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个 Socket。建立网络通信连接至少要一对端口号(Socket)。Socket的本质是编程接口(API),对 TCP/IP的封装,TCP/IP也要提供可供程序员做网络开发所用的接口,这就是Socket编程接口。如果 说HTTP是轿车,提供了封装或者显示数据的具体形式,那么Socket就是发动机,提供了网络通信 的能力。

2.1 Socket简介

Socket的英文原义是"孔"或"插座",作为BSD UNIX的进程通信机制,取后一种意思。 Socket通常也称作"套接字",用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不 同虚拟机或不同计算机之间的通信。在网络上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。

Socket正 如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座都有一个编 号,有的插座提供220伏交流电,有的提供110伏交流电,有的则提供有线电视节目。客户软件将 插头插到不同编号的插座中,就可以得到不同的服务。

Socket起源于Unix,而Unix的基本哲学之一就是"一切皆文件",都可以使用如下模式来 操作。

bash 复制代码
打开 -> 读写write/read -> 关闭close

Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件 描述符。Socket的类型有两种:流式Socket和数据报式Socket。流式是一种面向连接的Socket,针 对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用


2.2 TCP编程

2.2.1 TCP简介

TCP是Transmission Control Protocol的缩写,中文名是传输控制协议。它是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。在简化的计算机网络OSI模型中, 它完成第四层传输层所指定的功能,用户数据报协议(UDP)是同一层内另一个重要的传输协议。

在互联网协议族中,TCP层是位于IP层之上、应用层之下的中间层。不同主机的应用层之间经常需 要可靠的、像通道一样的连接,但是IP层不提供这样的流机制,而是提供不可靠的包交换。

TCP是因特网中的传输层协议,使用三次握手协议建立连接。当主动方发出SYN连接请求后, 等待对方回答SYN+ACK,并最终对对方的SYN执行ACK确认。这种建立连接的方法可以防止产生错误的连接。TCP使用的流量控制协议是可变大小的滑动窗口协议。TCP三次握手的过程如下:

这张图描述的是TCP(传输控制协议)的三次握手过程,这是建立TCP连接的标准方法。

  1. 第一步 - SYN:
  • 客户端发送一个SYN(同步)包到服务器。
  • SYN=1 表示这是一个同步请求。
  • seq=J 是客户端选择的初始序列号。
  1. 第二步 - SYN-ACK:
  • 服务器收到SYN后,回复一个SYN-ACK包。
  • SYN=1 表示这也是一个同步包。
  • ACK=1 表示这是一个确认包。
  • ack=J+1 是对客户端序列号的确认(下一个期望收到的序列号)。
  • seq=K 是服务器选择的初始序列号。
  1. 第三步 - ACK:
  • 客户端收到SYN-ACK后,发送一个ACK包作为响应。
  • ACK=1 表示这是一个确认包。
  • ack=K+1 是对服务器序列号的确认。
  1. 连接建立:
  • 完成这三步后,TCP连接就建立了。
  • 双方都标记连接为ESTABLISHED状态。

这个过程的目的是:

  • 同步双方的初始序列号。
  • 确保双方都能发送和接收数据。
  • 避免旧的或重复的连接干扰新连接。

这种机制保证了连接的可靠性和数据传输的有序性,是TCP协议可靠传输特性的基础。

2.2.2 TCP的关键特性

  1. 分段传输: 数据被分割成适合的数据块。
  2. 确认机制 :
    • 发送方启动定时器等待确认
    • 接收方发送确认
  3. 校验和: 检测传输中的数据变化。
  4. 排序重组: 处理失序到达的数据包。
  5. 去重: 丢弃重复的数据包。
  6. 流量控制: 防止缓冲区溢出。
  7. 可靠传输: 通过以上机制确保数据可靠传输。

2.2.3 TCP客户端

Go语言提供了net包来实现Socket编程,大部分使用者只需要Dial、Listen和Accept函数提供的基本接口,以及相关的Conn和Listener接口。

对于网络编程而言,推荐使用log包代替fmt包进行打印信息,log包打印时,会附加打印出 时间,方便调试程序。log.Fatal表示当遇到严重错误时打印错误信息,并停止程序的运行。

对于TCP和UDP网络,地址格式是"host:port"或"[host]:port",例如:

Go 复制代码
package main

import (
	"log"
	"net"
)

func main() {
	// 尝试连接百度服务器
	conn, err := net.Dial("tcp", "www.baidu.com:80")
	// 连接本地端口
	// conn, err := net.Dial("tcp", ":1234")
	if err != nil {
		log.Fatal("连接失败!", err)
	}
	defer conn.Close()
	log.Println("连接成功!")
}

开启了一个对百度服务器的80端口的TCP连接,如果没报错,就表示连接成功,程序运行后输出如下:

bash 复制代码
2024/07/19 00:10:13 连接成功!

Dial函数在连接时,如果端口未开放,尝试连接就会立刻返回服务器拒绝连接的错误。

尝试连接本地(127.0.0.1)的1234端口,由于该端口未开放任何TCP服务,程序就会抛出连接失败的信息,如下所示:

bash 复制代码
2024/07/19 00:12:49 连接失败!dial tcp :1234: connectex: No connection could be made because the target machine actively refused it.

有时我们会遇到这种情况:需要连接的TCP服务开放着,但由于网络或者防火墙的原因,导致始终无法连接成功。这时我们需要设置超时时间来避免程序一直阻塞运行,设置超时可以使用 DialTimeout函数。

HTTP协议页是基于TCP的Socket协议实现的,因此可以使用TCP客户端来请求百度的HTTP服务。

Go 复制代码
package main

import (
	"log"
	"net"
)

func main() {
	// 尝试连接百度服务器
	conn, err := net.Dial("tcp", "www.baidu.com:80")
	if err != nil {
		log.Fatal("连接失败!", err)
	}
	defer conn.Close()
	log.Println("连接成功!")

	// 发送HTTP形式的内容
	conn.Write([]byte("GET / HTTP/1.1\r\nHost: www.baidu.com\r\nUser-Agent: curl/7.55.1\r\nAccept: */*\r\n\r\n"))
	log.Println("发送HTTP请求成功!")
	var buf = make([]byte, 1024)
	conn.Read(buf)
	log.Println(string(buf))
}

连接百度服务器的80端口,并向80端口发送了HTTP请求包,模拟了一次HTTP请求,百度服务器接收到并成功解析该请求后,就会做出响应,程序运行结果如下:

bash 复制代码
2024/07/19 00:59:26 连接成功!
2024/07/19 00:59:26 发送HTTP请求成功!
2024/07/19 00:59:27 HTTP/1.1 200 OK
Accept-Ranges: bytes
...
Set-Cookie: BDORZ=27315; max-age=86400; domain=.baidu.com; path=/

<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link
...

2.2.4 TCP服务端

  • 将服务器代码保存为 server.go,客户端代码保存为 client.go
  • 在一个终端中运行服务器,在另一个终端中运行客户端
  • 在客户端终端中输入消息并按回车发送。你会看到服务器的响应。
  • 你可以运行多个客户端实例来测试多客户端场景。

服务端

Go 复制代码
// 服务器代码 (server.go)
package main

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

func main() {
	// 在8080端口上创建TCP监听器
	listener, err := net.Listen("tcp", ":8080")
	if err != nil {
		fmt.Println("监听错误:", err)
		return
	}
	defer listener.Close() // 确保在函数结束时关闭监听器
	fmt.Println("服务器正在监听8080端口")

	for {
		// 接受新的客户端连接
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("接受连接错误:", err)
			continue
		}
		fmt.Println("新客户端连接:", conn.RemoteAddr())
		// 为每个客户端启动一个新的goroutine来处理连接
		go handleConnection(conn)
	}
}

func handleConnection(conn net.Conn) {
	defer conn.Close() // 确保在函数结束时关闭连接
	reader := bufio.NewReader(conn)

	for {
		// 设置30秒的读取超时
		conn.SetReadDeadline(time.Now().Add(30 * time.Second))

		// 读取客户端发送的消息,直到遇到换行符
		message, err := reader.ReadString('\n')
		if err != nil {
			if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
				fmt.Println("读取超时,关闭连接")
			} else {
				fmt.Println("读取错误:", err)
			}
			return
		}

		fmt.Printf("收到来自 %s 的消息: %s", conn.RemoteAddr(), message)

		// 设置10秒的写入超时
		conn.SetWriteDeadline(time.Now().Add(10 * time.Second))

		// 向客户端发送确认消息
		_, err = conn.Write([]byte("已接收: " + message))
		if err != nil {
			fmt.Println("写入错误:", err)
			return
		}
	}
}

客户端

Go 复制代码
// 客户端代码 (client.go)
package main

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

func main() {
	// 连接到服务器
	conn, err := net.Dial("tcp", "localhost:8080")
	if err != nil {
		fmt.Println("连接错误:", err)
		return
	}
	defer conn.Close() // 确保在函数结束时关闭连接

	fmt.Println("连接成功,请输入消息,按回车键发送") // 连接成功后的提示

	// 启动一个goroutine来接收服务器消息
	go receiveMessages(conn)
	// 在主goroutine中发送消息
	sendMessages(conn)
}

func receiveMessages(conn net.Conn) {
	reader := bufio.NewReader(conn)
	for {
		// 设置60秒的读取超时
		conn.SetReadDeadline(time.Now().Add(60 * time.Second))

		// 读取服务器发送的消息,直到遇到换行符
		message, err := reader.ReadString('\n')
		if err != nil {
			if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
				fmt.Println("读取超时,未收到服务器消息")
				continue
			}
			fmt.Println("读取错误:", err)
			return
		}
		fmt.Print("服务器: ", message)
	}
}

func sendMessages(conn net.Conn) {
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		message := scanner.Text()
		// 设置10秒的写入超时
		conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
		// 发送消息到服务器
		_, err := conn.Write([]byte(message + "\n"))
		if err != nil {
			fmt.Println("发送消息错误:", err)
			return
		}
	}
}

2.3 UDP编程

2.3.1 UDP简介

UDP是User Datagram Protocol的缩写,中文名是用户数据报协议。它是OSI参考模型中一种无连 接的传输层协议,提供面向事务的简单不可靠信息传送服务。

UDP协议在网络中与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中位于 第四层------传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排 序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。UDP用来支持那些需 要在计算机之间传输数据的网络应用,包括网络视频会议系统在内的众多的客户/服务器模式的网 络应用。UDP协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩 盖,但是即使是在今天,UDP仍然不失为一项非常实用和可行的网络传输层协议。

2.3.2 UDP客户端

与TCP客户端类似,创建一个UDP客户端同样使用Dial函数,只需在参数中声明发起的请求协 议为UDP即可

bash 复制代码
package main

import (
	"log"
	"net"
)

func main() {
	// 尝试连接本地1234端口
	conn, err := net.Dial("udp", ":1234")
	if err != nil {
		log.Fatal("连接失败!", err)
	}
	defer conn.Close()
	log.Println("连接成功!")
}

尝试连接本地UDP的1234端口,该端口是关闭的,打印输出的结果如下:

bash 复制代码
024/07/19 02:22:08 连接成功!

由于UDP是无连接的协议,只关心信息是否成功发送,不关心对方是否成功接收,只要消息 报文发送成功,就不会报错,因此会输出连接成功的信息

2.3.3 UDP服务端

与TCP服务端不同,创建一个UDP服务端无法使用有连接的Listen函数,而要使用无连接的 ListenUDP函数。

基于UDP的协议有很多,如DNS域名解析服务、NTP网络时间协议等。我们来模拟一个最简单的NTP服务器,每当接收到任意字节的信息,就将当前的时间发送给客户端。

服务器 (server.go):

  1. 在指定端口(8123)上创建UDP监听器。
  2. 使用无限循环持续监听客户端请求。
  3. 当收到任何消息时,获取当前时间并发送回客户端。
Go 复制代码
// 服务器代码 (server.go)
package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	// 在8123端口上监听UDP连接
	addr, err := net.ResolveUDPAddr("udp", ":8123")
	if err != nil {
		fmt.Println("地址解析错误:", err)
		return
	}

	conn, err := net.ListenUDP("udp", addr)
	if err != nil {
		fmt.Println("监听错误:", err)
		return
	}
	defer conn.Close()

	fmt.Println("NTP服务器正在监听 :8123")

	for {
		handleClient(conn)
	}
}

func handleClient(conn *net.UDPConn) {
	buffer := make([]byte, 1024)

	// 读取客户端发送的数据
	_, remoteAddr, err := conn.ReadFromUDP(buffer)
	if err != nil {
		fmt.Println("读取数据错误:", err)
		return
	}

	fmt.Printf("收到来自 %s 的请求\n", remoteAddr)

	// 获取当前时间并格式化
	currentTime := time.Now().Format(time.RFC3339)

	// 发送时间信息给客户端
	_, err = conn.WriteToUDP([]byte(currentTime), remoteAddr)
	if err != nil {
		fmt.Println("发送响应错误:", err)
	}
}

客户端 (client.go):

  1. 连接到指定的服务器地址和端口。
  2. 发送一个简单的消息("获取时间")到服务器。
  3. 等待并接收服务器的响应(当前时间)。
  4. 打印接收到的时间信息。
Go 复制代码
// 客户端代码 (client.go)
package main

import (
	"fmt"
	"net"
)

func main() {
	// 服务器地址
	serverAddr, err := net.ResolveUDPAddr("udp", "localhost:8123")
	if err != nil {
		fmt.Println("地址解析错误:", err)
		return
	}

	// 创建UDP连接
	conn, err := net.DialUDP("udp", nil, serverAddr)
	if err != nil {
		fmt.Println("连接错误:", err)
		return
	}
	defer conn.Close()

	// 发送任意消息给服务器
	_, err = conn.Write([]byte("获取时间"))
	if err != nil {
		fmt.Println("发送请求错误:", err)
		return
	}

	// 接收服务器响应
	buffer := make([]byte, 1024)
	n, _, err := conn.ReadFromUDP(buffer)
	if err != nil {
		fmt.Println("接收响应错误:", err)
		return
	}

	// 打印接收到的时间
	fmt.Printf("服务器时间: %s\n", string(buffer[:n]))
}

使用:

  1. 先运行服务器:go run server.go
  2. 然后在另一个终端运行客户端:go run client.go
bash 复制代码
服务器时间: 2024-07-19T02:37:05-07:00
相关推荐
AI原吾2 小时前
掌握Python-uinput:打造你的输入设备控制大师
开发语言·python·apython-uinput
机器视觉知识推荐、就业指导2 小时前
Qt/C++事件过滤器与控件响应重写的使用、场景的不同
开发语言·数据库·c++·qt
毕设木哥2 小时前
25届计算机专业毕设选题推荐-基于python的二手电子设备交易平台【源码+文档+讲解】
开发语言·python·计算机·django·毕业设计·课程设计·毕设
珞瑜·2 小时前
Matlab R2024B软件安装教程
开发语言·matlab
weixin_455446172 小时前
Python学习的主要知识框架
开发语言·python·学习
孤寂大仙v2 小时前
【C++】STL----list常见用法
开发语言·c++·list
她似晚风般温柔7893 小时前
Uniapp + Vue3 + Vite +Uview + Pinia 分商家实现购物车功能(最新附源码保姆级)
开发语言·javascript·uni-app
咩咩大主教3 小时前
C++基于select和epoll的TCP服务器
linux·服务器·c语言·开发语言·c++·tcp/ip·io多路复用
FuLLovers3 小时前
2024-09-13 冯诺依曼体系结构 OS管理 进程
linux·开发语言
王中阳Go3 小时前
字节跳动的微服务独家面经
微服务·面试·golang