目录
一、TCP/IP协议族和四层模型概述
1.1 互联网协议族(TCP/IP)
1.2 TCP/IP四层模型
1. 网络访问层(Network Access Layer)
2. 网络层(Internet Layer)
3. 传输层(Transport Layer)
4. 应用层(Application Layer)
1.3 特点和作用
二、Socket基础
2.1 Socket简介
2.2 TCP编程
2.2.1 TCP简介
2.2.2 TCP的关键特性
2.2.3 TCP客户端
2.2.4 TCP服务端
2.3 UDP编程
2.3.1 UDP简介
2.3.2 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的基本哲学之一就是“一切皆文件”,都可以使用如下模式来 操作。
打开 -> 读写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 是客户端选择的初始序列号。
2. 第二步 - SYN-ACK:
- 服务器收到SYN后,回复一个SYN-ACK包。
- SYN=1 表示这也是一个同步包。
- ACK=1 表示这是一个确认包。
- ack=J+1 是对客户端序列号的确认(下一个期望收到的序列号)。
- seq=K 是服务器选择的初始序列号。
3. 第三步 - ACK:
- 客户端收到SYN-ACK后,发送一个ACK包作为响应。
- ACK=1 表示这是一个确认包。
- ack=K+1 是对服务器序列号的确认。
4. 连接建立:
- 完成这三步后,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”,例如:
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连接,如果没报错,就表示连接成功,程序运行后输出如下:
2024/07/19 00:10:13 连接成功!
Dial函数在连接时,如果端口未开放,尝试连接就会立刻返回服务器拒绝连接的错误。
尝试连接本地(127.0.0.1)的1234端口,由于该端口未开放任何TCP服务,程序就会抛出连接失败的信息,如下所示:
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服务。
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请求,百度服务器接收到并成功解析该请求后,就会做出响应,程序运行结果如下:
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
。 - 在一个终端中运行服务器,在另一个终端中运行客户端
- 在客户端终端中输入消息并按回车发送。你会看到服务器的响应。
- 你可以运行多个客户端实例来测试多客户端场景。
服务端
// 服务器代码 (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
}
}
}
客户端
// 客户端代码 (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即可
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端口,该端口是关闭的,打印输出的结果如下:
024/07/19 02:22:08 连接成功!
由于UDP是无连接的协议,只关心信息是否成功发送,不关心对方是否成功接收,只要消息 报文发送成功,就不会报错,因此会输出连接成功的信息
2.3.3 UDP服务端
与TCP服务端不同,创建一个UDP服务端无法使用有连接的Listen函数,而要使用无连接的 ListenUDP函数。
基于UDP的协议有很多,如DNS域名解析服务、NTP网络时间协议等。我们来模拟一个最简单的NTP服务器,每当接收到任意字节的信息,就将当前的时间发送给客户端。
服务器 (server.go):
- 在指定端口(8123)上创建UDP监听器。
- 使用无限循环持续监听客户端请求。
- 当收到任何消息时,获取当前时间并发送回客户端。
// 服务器代码 (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):
- 连接到指定的服务器地址和端口。
- 发送一个简单的消息("获取时间")到服务器。
- 等待并接收服务器的响应(当前时间)。
- 打印接收到的时间信息。
// 客户端代码 (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
服务器时间: 2024-07-19T02:37:05-07:00