golang随笔3
1. select 语句
1. 什么是select:
select
是 Go 语言中用于实现多路复用的关键字。它可以监视多个通道或文件描述符,并在其中任何一个就绪时执行相应的操作。
2. select 的工作原理:
select
语句会阻塞,直到它监视的某个通道或文件描述符就绪。- 当某个通道或文件描述符就绪时,
select
语句会随机选择一个就绪的通道或文件描述符,并执行与该通道或文件描述符关联的case语句。 - 如果没有通道或文件描述符就绪,
select
语句会一直阻塞,直到有通道或文件描述符就绪。
3. select 的语法:
Go
select {
case <-ch1:
// 处理来自ch1的数据
case <-ch2:
// 处理来自ch2的数据
default:
// 没有通道就绪时执行的操作
}
4. select 的应用场景:
- 网络 I/O: 可以使用
select
来监视多个网络连接,并在其中任何一个连接可读或可写时执行相应的操作。 - 定时器: 可以使用
select
来实现定时器功能。 - 并发控制: 可以使用
select
来实现并发控制,例如互斥锁和条件变量。
5. select 的注意事项:
select
语句中的 case 语句必须是顺序执行的。select
语句中的 default 语句是可选的,如果省略了 default 语句,则当没有通道或文件描述符就绪时,select
语句会一直阻塞。select
语句可以与goroutine
结合使用,以实现更复杂的并发操作。
以下是一些select的应用示例:
- 网络 I/O:
Go
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
defer listener.Close()
for {
conn, err := listener.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
log.Fatal(err)
}
// 处理数据
_, err = conn.Write(buf[:n])
if err != nil {
log.Fatal(err)
}
}
}
- 定时器:
Go
func main() {
timer := time.NewTimer(time.Second)
select {
case <-timer.C:
// 定时器超时
}
}
以下是一个使用 select
语句的简单示例:
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- "Hello"
}()
go func() {
time.Sleep(3 * time.Second)
ch2 <- "World"
}()
select {
case msg1 := <-ch1:
fmt.Println("Received:", msg1)
case msg2 := <-ch2:
fmt.Println("Received:", msg2)
case <-time.After(1 * time.Second):
fmt.Println("Timeout occurred")
}
}
在这个示例中,我们创建了两个通道 ch1
和 ch2
,然后分别在不同的 Go 协程中向它们发送消息。使用 select
语句来等待两个通道的消息,并在其中一个通道接收到消息时打印出来。如果某个通道在1秒内都没有消息,则会触发超时。‘
原子操作
概念: 在Go语言中,原子操作是一种特殊的操作,它们能够确保在并发环境下对共享数据的读取、修改和存储是不可中断的,从而避免了竞态条件和数据竞争。
解释: 原子操作是指在单个CPU指令中执行的操作,它们保证了对共享数据的操作是原子性的,即不会被中断或干扰。这意味着其他goroutine
无法同时访问或修改同一数据,从而确保了数据的一致性和可靠性。
使用方法: 在Go语言中,可以使用sync/atomic
包提供的原子操作函数来进行原子操作。这些函数能够对基本数据类型(如整数和指针)执行原子读取、写入、增加、减少等操作。一些常见的原子操作函数包括AddXXX
、LoadXXX
、StoreXXX
等,其中XXX
代表不同的数据类型。
使用场景:
- 计数器和计时器: 当需要在并发环境下对计数器或计时器进行操作时,可以使用原子操作来确保操作的一致性。
- 状态标志: 在需要进行状态判断或更新时,可以使用原子操作来避免竞态条件,保证状态的正确性。
使用考虑:
- 性能优化: 原子操作通常比锁定更轻量级,因此在高并发场景下可能更具性能优势。
- 适用范围: 原子操作适用于对单个变量的简单操作,复杂的操作可能需要使用锁或其他同步机制来实现。
package main
import (
"fmt"
"sync/atomic"
"time"
)
var counter int32 // 使用int32类型的计数器
func increment() {
atomic.AddInt32(&counter, 1) // 原子增加计数器的值
fmt.Println("Counter Incremented to:", atomic.LoadInt32(&counter)) // 原子读取计数器的值
}
func main() {
for i := 0; i < 10; i++ {
go increment()
}
time.Sleep(time.Second) // 等待goroutine完成
fmt.Println("Final Counter Value:", atomic.LoadInt32(&counter)) // 原子读取最终的计数器值
}
方法 | 解释 |
---|---|
func LoadInt32(addr *int32) (val int32)func LoadInt64(addr *int64) (val int64)func LoadUint32(addr *uint32) (val uint32)func LoadUint64(addr *uint64) (val uint64)func LoadUintptr(addr *uintptr) (val uintptr)func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer) | 读取操作 |
func StoreInt32(addr *int32, val int32)func StoreInt64(addr *int64, val int64)func StoreUint32(addr *uint32, val uint32)func StoreUint64(addr *uint64, val uint64)func StoreUintptr(addr *uintptr, val uintptr)func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer) | 写入操作 |
func AddInt32(addr *int32, delta int32) (new int32)func AddInt64(addr *int64, delta int64) (new int64)func AddUint32(addr *uint32, delta uint32) (new uint32)func AddUint64(addr *uint64, delta uint64) (new uint64)func AddUintptr(addr *uintptr, delta uintptr) (new uintptr) | 修改操作 |
func SwapInt32(addr *int32, new int32) (old int32)func SwapInt64(addr *int64, new int64) (old int64)func SwapUint32(addr *uint32, new uint32) (old uint32)func SwapUint64(addr *uint64, new uint64) (old uint64)func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer) | 交换操作 |
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool) | 比较并交换操作 |
package main
import (
"fmt"
"sync"
"sync/atomic"
"time"
)
type Counter interface {
Inc()
Load() int64
}
// 普通版
type CommonCounter struct {
counter int64
}
func (c CommonCounter) Inc() {
c.counter++
}
func (c CommonCounter) Load() int64 {
return c.counter
}
// 互斥锁版
type MutexCounter struct {
counter int64
lock sync.Mutex
}
func (m *MutexCounter) Inc() {
m.lock.Lock()
defer m.lock.Unlock()
m.counter++
}
func (m *MutexCounter) Load() int64 {
m.lock.Lock()
defer m.lock.Unlock()
return m.counter
}
// 原子操作版
type AtomicCounter struct {
counter int64
}
func (a *AtomicCounter) Inc() {
atomic.AddInt64(&a.counter, 1)
}
func (a *AtomicCounter) Load() int64 {
return atomic.LoadInt64(&a.counter)
}
func test(c Counter) {
var wg sync.WaitGroup
start := time.Now()
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
c.Inc()
wg.Done()
}()
}
wg.Wait()
end := time.Now()
fmt.Println(c.Load(), end.Sub(start))
}
func main() {
c1 := CommonCounter{} // 非并发安全
test(c1)
c2 := MutexCounter{} // 使用互斥锁实现并发安全
test(&c2)
c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高
test(&c3)
}
context
作用
在 Go 语言中,上下文(Context
)是一个标准库中的类型,用于在函数之间传递请求作用域的数据、取消信号和超时。
上下文主要用于以下几个方面:
-
请求作用域的数据传递: 在一个请求处理过程中,可能会涉及多个函数之间的调用,这些函数需要共享一些请求相关的信息,比如请求的 ID、用户身份验证信息等。通过上下文,可以将这些信息传递给所有相关的函数,而不必每个函数都显式地传递这些参数。
-
取消信号: 上下文可以用于在请求处理过程中传递取消信号,以便在某些情况下中止正在进行的操作。这在处理超时请求或者收到中断信号时特别有用。
-
超时控制: 上下文还可以用于设置操作的超时时间。如果某个操作在超时时间内未完成,可以通过上下文取消信号来中止该操作。
在 Go 中,上下文通常是通过 context.Context
类型来表示的,标准库提供了一系列与上下文相关的函数和方法,例如 context.WithCancel()
、context.WithTimeout()
、context.WithDeadline()
等,用于创建具有取消和超时功能的上下文。
总之,上下文提供了一种有效的方式来管理请求作用域的数据、处理取消信号和控制操作的超时,从而提高了程序的可靠性和可控性。
示例
package main
import (
"context"
"fmt"
"time"
)
// worker 接收到上下文的 停止信号 则停止工作
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s 收到取消信号 , 停止工作 \n", name)
return
default:
fmt.Printf("%s 正在工作 ... \n", name)
time.Sleep(1 * time.Second)
}
}
}
func main() {
// 创建一个父级上下文
parentCtx := context.Background()
// 创建一个 3秒钟自动取消的上下文
ctx, cancelFunc := context.WithTimeout(parentCtx, 3*time.Second)
defer cancelFunc()
// 开启两个goroutine
go worker(ctx, "worker1")
go worker(ctx, "worker2")
// 等待时间 手动取消上下文
time.Sleep(2 * time.Second)
// 手动调用 cancel() 向上下文中 发送取消信号
cancelFunc() // 手动取消上下文
time.Sleep(2 * time.Second)
fmt.Println("主程序退出")
}
常用方法
下面是 context
包中常用的方法及其含义:
方法 | 含义 |
---|---|
context.Background() | 返回一个空的 Context ,通常用作根 Context 。 |
context.TODO() | 返回一个非空的 Context ,表示不确定的 Context 。 |
context.WithCancel() | 返回父上下文的副本,并创建一个新的 CancelFunc 。当调用该函数时,会取消该 Context 。 |
context.WithDeadline() | 返回父上下文的副本,并设置截止时间,超过截止时间后,该 Context 会自动被取消。 |
context.WithTimeout() | 返回父上下文的副本,并设置超时时间,超过超时时间后,该 Context 会自动被取消。 |
context.WithValue() | 返回父上下文的副本,并设置一个键值对,可以在 Context 中存储和传递请求作用域数据。 |
ctx.Done() | 返回一个 chan ,当 Context 被取消或者超时时,该 chan 会被关闭。 |
ctx.Err() | 返回取消原因,如果 Context 没有被取消,则返回 nil 。 |
ctx.Value(key interface{}) | 返回与键关联的值,如果 Context 中不存在该键,则返回 nil 。 |
这些方法可以帮助我们有效地使用 Context
,实现并发控制、超时处理和请求作用域数据的传递。
errorGroup
errgroup
包是 Go 语言标准库 golang.org/x/sync/errgroup
提供的一个有用工具,用于在并发任务中管理多个 goroutine,并能够捕获这些 goroutine 中的错误。
该包中最重要的组件是 errgroup.Group
结构体,它提供了管理和等待 goroutine 完成的功能,并能够在任何一个 goroutine 出现错误时取消所有任务。
下面是 errgroup.Group
结构体的主要方法:
方法 | 描述 |
---|---|
func (g *Group) Go(f func() error) | 启动一个新的 goroutine 来执行给定的函数,函数的返回值为 error 类型。如果函数返回非 nil 的错误,则所有 goroutine 都会被取消。 |
func (g *Group) Wait() error | 等待所有 goroutine 完成,并返回第一个出现的错误,如果没有出现错误则返回 nil。 |
func (g Group) WithContext(ctx context.Context) (Group, context.Context) | 返回一个新的 Group 和继承自指定上下文的上下文,这样新创建的 goroutine 将继承该上下文。 |
下面是一个示例代码,演示了如何使用 errgroup.Group
来管理并发任务,并捕获其中的错误:
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
// 创建一个带有上下文的 Group
g, ctx := errgroup.WithContext(context.Background())
// 启动多个 goroutine 来执行任务
for i := 0; i < 3; i++ {
id := i
g.Go(func() error {
// 模拟执行任务,这里将第二个任务模拟为失败的情况
if id == 1 {
return fmt.Errorf("goroutine %d 执行失败", id)
}
fmt.Printf("goroutine %d 执行成功\n", id)
return nil
})
}
// 等待所有任务完成
if err := g.Wait(); err != nil {
fmt.Println("出现错误:", err)
} else {
fmt.Println("所有任务执行成功")
}
}
在这个示例中,
创建了一个带有上下文的 errgroup.Group
,然后启动了三个 goroutine 来执行任务。其中第二个任务被模拟为失败。
最后,我们调用 Wait()
方法来等待所有任务完成,并检查是否有任何错误发生。
网络编程
OSI(开放系统互联)模型是一个用于理解和描述网络通信的框架,它将网络通信划分为七个层次,每个层次负责不同的功能。下面是 OSI 七层模型的详细解释:
下面是使用表格形式输出的 OSI 七层模型的详细解释:
层级 | 名称 | 功能 | |
---|---|---|---|
7 | 应用层(Application Layer) | 提供应用程序间的通信和数据交换。 | 提供了各种应用程序和网络服务,如电子邮件、文件传输、网页浏览等。 |
6 | 表示层(Presentation Layer) | 数据的格式化、加密和压缩。 | 使不同系统之间的数据能够互相理解和交换。 |
5 | 会话层(Session Layer) | 建立、管理和终止会话(或连接)。 | 提供了在两个应用程序之间建立通信会话所需的协议和机制。 |
4 | 传输层(Transport Layer) | 端到端的数据传输,提供数据可靠性、流量控制和拥塞控制。 | 提供了数据可靠性、流量控制和拥塞控制等功能,通常通过 TCP(传输控制协议)或 UDP(用户数据报协议)实现。 |
3 | 网络层(Network Layer) | 选择最佳路径传输数据,实现逻辑地址分配和路由选择。 | 实现了逻辑地址(如 IP 地址)的分配和路由选择,以便在源和目的地之间进行通信。 |
2 | 数据链路层(Data Link Layer) | 直接相连节点之间的数据链路,处理数据帧的传输、同步和错误检测。 | 负责数据的帧同步、流量控制、错误检测和纠正等。 |
1 | 物理层(Physical Layer) | 传输原始比特流,处理传输介质的物理特性,如电压、速率、连接器类型等。 | 处理与传输介质(如电缆、光纤)相关的物理特性,如电压、信号速率、连接器类型等。 |
网络模型中的HTTP协议在通信过程中会添加各个部首,这些部首通常包括请求头和响应头。下面是HTTP通信过程中的主要步骤及部首的详细解释:
步骤 | 描述 | 举例 |
---|---|---|
1.建立连接 | 客户端向服务器发送连接请求。 | TCP握手 |
2.发送请求 | 客户端向服务器发送HTTP请求,包括请求行、请求头和请求体。 | GET /index.html HTTP/1.1 |
3.接收请求 | 服务器接收到HTTP请求,准备处理请求,并返回HTTP响应。 | |
4.发送响应 | 服务器向客户端发送HTTP响应,包括状态行、响应头和响应体。 | HTTP/1.1 200 OK |
5.接收响应 | 客户端接收到HTTP响应,处理响应数据。 | |
6.关闭连接 | 双方根据需要决定是否关闭连接。 |
下面是HTTP请求和响应中常见的部首:
- 请求行:包括请求方法、请求URL和HTTP协议版本。
- 请求头:包括客户端向服务器传递的附加信息,如User-Agent、Accept、Content-Type等。
- 响应行:包括HTTP协议版本、状态码和状态消息。
- 响应头:包括服务器向客户端传递的附加信息,如Server、Content-Type、Content-Length等。
HTTP请求和响应的部首在通信过程中起着非常重要的作用,它们包含了关于请求和响应的各种元信息,帮助客户端和服务器进行正确的处理和解析。
socket编程
Socket编程是一种在网络通信中常用的编程方式,它允许不同计算机上的程序之间进行数据交换和通信。Socket编程通常涉及两种角色:服务器和客户端。服务器等待客户端的连接请求,而客户端则发送请求并与服务器建立连接,然后进行数据交换。
下面是Socket编程的主要步骤和涉及的关键概念:
-
创建Socket:在服务器端和客户端分别创建一个Socket对象,用于与对方建立连接和进行数据传输。
-
绑定地址和端口:在服务器端,将Socket对象绑定到一个IP地址和端口上,以便客户端能够通过该地址和端口连接到服务器。在客户端,通常不需要绑定地址,而是直接指定服务器的地址和端口。
-
监听连接请求:在服务器端,调用
listen()
方法开始监听客户端的连接请求。 -
接受连接:在服务器端,调用
accept()
方法接受客户端的连接请求,并返回一个新的Socket对象,用于与该客户端进行通信。 -
建立连接:在客户端,调用
connect()
方法向服务器发起连接请求,并建立连接。 -
数据交换:建立连接后,服务器和客户端之间可以通过Socket对象进行数据交换。通常使用
send()
和recv()
方法发送和接收数据。 -
关闭连接:通信结束后,服务器和客户端分别调用
close()
方法关闭Socket连接。
Socket编程可以基于不同的协议进行,如TCP(传输控制协议)和UDP(用户数据报协议)。TCP提供可靠的、面向连接的数据传输,适用于需要确保数据完整性和顺序性的场景;而UDP提供不可靠的、无连接的数据传输,适用于实时性要求较高、数据量较小的场景。
总的来说,Socket编程是一种灵活且功能强大的网络编程方式,可用于实现各种网络应用,如Web服务器、聊天程序、文件传输等。
Socket
是应用层与TCP/IP协议族通信的中间软件抽象层。在设计模式中,Socket
其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket
后面,对用户来说只需要调用Socket规定的相关函数,让Socket
去组织符合指定的协议数据然后进行通信。
概念
网络编程是指通过计算机网络进行数据交换和通信的程序开发。在Golang中,网络编程主要通过标准库中的net
包来实现。这个包提供了创建网络应用所需的各种功能和接口,包括操作TCP、UDP、Unix域套接字等。
下面是Golang中网络编程的一般步骤:
-
创建连接:在客户端,首先创建一个连接,通常是TCP或UDP连接。在服务器端,需要监听特定的网络地址和端口,等待客户端的连接请求。
-
数据传输:一旦连接建立,数据的传输就可以开始了。可以通过连接的读取和写入操作进行数据的发送和接收。
-
处理连接:服务器端需要处理多个客户端的连接请求,可以通过并发处理或使用事件驱动模型来实现。
-
关闭连接:在数据传输结束后,需要关闭连接,释放资源。
在Golang中,可以使用net
包中的net.Listen()
来监听连接请求,使用net.Dial()
来建立连接。一旦连接建立,可以使用net.Conn
接口来进行数据的读写操作。
另外,Golang的net/http
包提供了一个高级的HTTP服务器和客户端的实现,简化了HTTP协议的处理过程,使得开发HTTP应用更加便捷。
主要方法
下面是net/http
包中常用的一些方法:
方法名 | 说明 |
---|---|
http.ListenAndServe | 启动一个HTTP服务器并监听指定地址和端口 |
http.ListenAndServeTLS | 启动一个HTTPS服务器并监听指定地址和端口 |
http.HandleFunc | 注册一个HTTP请求处理函数 |
http.Handle | 注册一个HTTP请求处理器 |
http.FileServer | 返回一个处理静态文件的HTTP处理器 |
http.ServeFile | 返回一个处理单个文件的HTTP处理器 |
http.NotFound | 返回一个处理404错误的HTTP处理器 |
http.Redirect | 返回一个HTTP处理器,用于重定向请求 |
http.StripPrefix | 返回一个新的处理器,用于去掉URL路径的前缀 |
http.Error | 返回一个HTTP处理器,用于发送指定状态码的错误信息 |
http.Client | 创建一个新的HTTP客户端 |
http.NewRequest | 创建一个新的HTTP请求 |
http.Get | 发送一个GET请求 |
http.Post | 发送一个POST请求 |
http.Head | 发送一个HEAD请求 |
http.Do | 发送一个自定义方法的HTTP请求 |
http.Serve | 在指定的监听器上启动HTTP服务器 |
net包方法
方法 | 描述 |
---|---|
Dial | 建立到指定网络地址的连接 |
Listen | 在指定的网络地址上监听连接请求 |
ResolveIPAddr | 解析 IP 地址 |
ResolveTCPAddr | 解析 TCP 地址 |
ResolveUDPAddr | 解析 UDP 地址 |
ResolveUnixAddr | 解析 Unix 地址 |
SplitHostPort | 将地址字符串分割为主机和端口 |
JoinHostPort | 将主机和端口组合成一个地址字符串 |
LookupIP | 查询主机的 IP 地址 |
LookupPort | 查询服务的端口号 |
LookupAddr | 查询 IP 地址的域名 |
LookupCNAME | 查询域名的规范名 |
LookupHost | 查询域名对应的主机名 |
LookupMX | 查询域名的邮件交换服务器记录 |
LookupNS | 查询域名的 DNS 服务器记录 |
LookupTXT | 查询域名的文本记录 |
LookupSRV | 查询域名的服务记录 |
LookupAddrContext | 带有上下文的查询 IP 地址 |
LookupCNAMEContext | 带有上下文的查询域名的规范名 |
LookupHostContext | 带有上下文的查询域名对应的主机名 |
LookupMXContext | 带有上下文的查询域名的邮件交换服务器记录 |
LookupNSContext | 带有上下文的查询域名的 DNS 服务器记录 |
LookupTXTContext | 带有上下文的查询域名的文本记录 |
LookupSRVContext | 带有上下文的查询域名的服务记录 |
InterfaceAddrs | 返回本地系统接口的地址列表 |
InterfaceByIndex | 根据索引返回系统接口 |
Interfaces | 返回本地系统接口列表 |
ParseCIDR | 解析 CIDR 地址字符串 |
CIDRMask | 创建指定位数的 CIDR 掩码 |
IPv4Mask | 创建指定位数的 IPv4 掩码 |
IPMask | 创建指定位数的 IP 掩码 |
IPv4 | 将 4 个字节的切片转换为 IPv4 地址 |
ParseIP | 解析 IP 地址字符串 |
IP.Equal | 比较两个 IP 地址是否相等 |
IP.IsGlobalUnicast | 检查 IP 地址是否是全局单播地址 |
IP.IsInterfaceLocal | 检查 IP 地址是否是接口本地地址 |
IP.IsLinkLocalMulticast | 检查 IP 地址是否是链路本地多播地址 |
IP.IsLinkLocalUnicast | 检查 IP 地址是否是链路本地单播地址 |
IP.IsLoopback | 检查 IP 地址是否是环回地址 |
IP.IsMulticast | 检查 IP 地址是否是多播地址 |
IP.IsUnspecified | 检查 IP 地址是否是未指定地址 |
IP.String | 返回 IP 地址的字符串表示 |
IP.To4 | 将 IPv4 地址转换为 4 个字节的切片 |
IP.To16 | 将 IPv4 或 IPv6 地址转换为 16 个字节的切片 |
IPv6 | 将 16 个字节的切片转换为 IPv6 地址 |
TCPAddr | TCP 地址结构 |
TCPListener | TCP 监听器 |
TCPConn | TCP 连接 |
UDPAddr | UDP 地址结构 |
UDPConn | UDP 连接 |
UnixAddr | Unix 地址结构 |
UnixConn | Unix 连接 |
FileConn | 创建一个已经存在的文件描述符的连接 |
Pipe | 创建一个已经存在的管道的连接 |
示例
/* ------------------ server ---------------------- */
package main
import (
"fmt"
"net"
)
func main() {
listener, err := net.Listen("tcp", ":8080")
if err != nil {
fmt.Println("Error listening:", err.Error())
return
}
// 处理connection close error handler
defer func(listener net.Listener) {
err := listener.Close()
if err != nil {
fmt.Println("connetion close error occour:", err)
}
}(listener)
fmt.Println("Server is listening on port 8080")
// 在循环中 接受 链接数据
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting:", err.Error())
return
}
// 处理链接
go handleRequest(conn)
}
}
func handleRequest(conn net.Conn) {
defer func(conn net.Conn) {
err := conn.Close()
if err != nil {
fmt.Println("close connection:", err)
}
}(conn)
buffer := make([]byte, 1024)
for {
reqLen, err := conn.Read(buffer)
// 异常处理
if err != nil {
fmt.Println("error reading:", err)
return
}
fmt.Printf("Received message from client: %s \n", string(buffer[:reqLen]))
// 发送响应数据到客户端
_, err = conn.Write([]byte("Message received"))
if err != nil {
fmt.Println("Error writing:", err.Error())
return
}
}
}
/* ------------------ client ---------------------- */
package main
import (
"fmt"
"net"
"strconv"
"time"
)
func main() {
// 连接服务器
conn, err := net.Dial("tcp", "localhost:8080")
if err != nil {
fmt.Println("Error connecting:", err.Error())
return
}
defer func(conn net.Conn) {
err := conn.Close()
if err != nil {
fmt.Println("connection close error:", err)
}
}(conn)
i := 1
for {
message := "Hello , server! " + strconv.Itoa(i)
// 如果写入发生错误 , 则向连接中 发送错误信息
_, err = conn.Write([]byte(message))
if err != nil {
fmt.Println("Error sending...:", err.Error())
return
}
// 处理服务端响应
handleResponse(err, conn)
time.Sleep(3 * time.Second)
i++
}
}
// handleResponse 处理接受的异常数据
func handleResponse(err error, conn net.Conn) {
// 读取服务器响应
buffer := make([]byte, 1024)
respLen, err := conn.Read(buffer)
// 错误处理
if err != nil {
fmt.Println("Error receiving:", err.Error())
}
fmt.Printf("Received response from server : %s \n", string(buffer[:respLen]))
}
❗ TCP 粘包问题:
TCP粘包问题通常发生在数据发送方连续发送多个小数据包,而接收方在一次读取操作中无法区分这些数据包的边界,导致多个数据包被看作一个大的数据包,或者一个大的数据包被分割成多个小的数据包,造成粘包现象。
🔴原因:
TCP协议的工作机制:TCP协议是面向流的,它不保证数据包的边界,而是将数据流切分成适当大小的数据块进行传输。这意味着发送方发送的数据包并不会按照应用层发送的数据包进行划分,而是根据TCP缓冲区的大小和网络状况等因素来划分。
应用程序的读取方式:接收方的应用程序可能会使用不恰当的方式来读取数据,比如一次读取的字节数不足以容纳一个完整的数据包,或者没有正确处理粘包现象。
⚽解决方法:
使用消息边界:在数据包中添加消息边界标识,比如添加特殊的分隔符或者固定长度的头部信息来表示每个数据包的结束。接收方根据这些边界来识别每个数据包,从而解决粘包问题。
使用消息长度:在数据包的头部添加消息长度信息,表示后续数据的长度。接收方根据消息长度来读取数据,确保每次读取的数据长度正确,从而避免粘包问题。
(封包)采用定长数据包:固定每个数据包的长度,不管数据内容的实际长度如何,都发送固定长度的数据包。接收方根据固定长度来读取数据,从而避免粘包问题。
使用缓冲区:接收方可以使用缓冲区来缓存接收到的数据,等待完整的数据包再进行处理,从而避免因为数据包不完整而导致的粘包问题。
应用层协议设计:在设计应用层协议时,考虑到TCP协议的特性,合理规划消息格式和消息边界,从而减少粘包问题的发生。
📧封包解决原理
封包是解决TCP粘包问题的一种常见方法,其原理是在发送数据之前,将数据按照一定的规则封装成固定格式的数据包,然后发送给接收方。接收方在接收到数据包后,根据固定的格式解析数据包,从而正确地识别和处理每个数据包,避免粘包问题的发生。
封包的原理主要包括以下几个步骤:
数据封装:发送方将要发送的数据按照一定的格式封装成数据包。这个格式通常包括数据的头部信息和数据内容两部分。头部信息可以包括数据长度、消息类型、校验码等信息,用于描述数据的属性和特征,以便接收方正确解析数据包。
数据发送:发送方将封装好的数据包通过TCP连接发送给接收方。由于数据包已经按照固定格式进行封装,因此即使在网络传输过程中发生数据拆分或合并,接收方仍然可以通过解析数据包头部信息来正确识别每个数据包的边界和内容。
数据解析:接收方收到数据包后,根据固定的格式解析数据包,提取出数据的头部信息和内容。通过头部信息中的数据长度等信息,接收方可以准确地判断每个数据包的边界和长度,从而正确地处理每个数据包,避免粘包问题的发生。
总的来说,封包的原理就是在数据传输的两端都采用相同的封装和解析规则,通过对数据进行封装和解析,保证数据在传输过程中的完整性和准确性,从而解决TCP粘包问题。
本文来自博客园,作者:{zhongweiLeex},转载请注明原文链接:{https://www.cnblogs.com/lzw6/}