目录
[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 特点和作用)
[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连接的标准方法。
- 第一步 - SYN:
- 客户端发送一个SYN(同步)包到服务器。
- SYN=1 表示这是一个同步请求。
- seq=J 是客户端选择的初始序列号。
- 第二步 - SYN-ACK:
- 服务器收到SYN后,回复一个SYN-ACK包。
- SYN=1 表示这也是一个同步包。
- ACK=1 表示这是一个确认包。
- ack=J+1 是对客户端序列号的确认(下一个期望收到的序列号)。
- seq=K 是服务器选择的初始序列号。
- 第三步 - ACK:
- 客户端收到SYN-ACK后,发送一个ACK包作为响应。
- ACK=1 表示这是一个确认包。
- ack=K+1 是对服务器序列号的确认。
- 连接建立:
- 完成这三步后,TCP连接就建立了。
- 双方都标记连接为ESTABLISHED状态。
这个过程的目的是:
- 同步双方的初始序列号。
- 确保双方都能发送和接收数据。
- 避免旧的或重复的连接干扰新连接。
这种机制保证了连接的可靠性和数据传输的有序性,是TCP协议可靠传输特性的基础。
2.2.2 TCP的关键特性
- 分段传输: 数据被分割成适合的数据块。
- 确认机制 :
- 发送方启动定时器等待确认
- 接收方发送确认
- 校验和: 检测传输中的数据变化。
- 排序重组: 处理失序到达的数据包。
- 去重: 丢弃重复的数据包。
- 流量控制: 防止缓冲区溢出。
- 可靠传输: 通过以上机制确保数据可靠传输。
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):
- 在指定端口(8123)上创建UDP监听器。
- 使用无限循环持续监听客户端请求。
- 当收到任何消息时,获取当前时间并发送回客户端。
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):
- 连接到指定的服务器地址和端口。
- 发送一个简单的消息("获取时间")到服务器。
- 等待并接收服务器的响应(当前时间)。
- 打印接收到的时间信息。
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]))
}
使用:
- 先运行服务器:
go run server.go
- 然后在另一个终端运行客户端:
go run client.go
bash
服务器时间: 2024-07-19T02:37:05-07:00