2025年golang:socket编程详解

golang:socket编程详解网络编程有两种 tcp socket 编程 是网络编程的主流 之所以叫做 tcp socket 编程 是因为底层是基于 tcpip 协议的 b s 结构的 http 编程 我们使用浏览器去访问服务器 使用的就是 http 协议 而 http 底层使用的依旧是 tcp socket 实现的 socket 编程

大家好,我是讯享网,很高兴认识大家。

网络编程有两种:

  • tcp socket编程,是网络编程的主流。之所以叫做tcp socket编程,是因为底层是基于tcpip协议的
  • b/s结构的http编程,我们使用浏览器去访问服务器,使用的就是http协议,而http底层使用的依旧是tcp socket实现的

socket编程

socket的作用

socket可以通过网络连接让多个进程建立通信并且相互传输数据,不管进程是不是在一个主机上。这意味这socket可以用来提供网络中不同计算机的多个应用程序的通信,可以用于单个计算机上的多个应用程序之间通信。不过,大部分都是在网络中通信【网络通信是基于TCP/IP协议】

socket的基本特性

操作系统内核提供了socket接口。在linux上,存在一个名为socket的系统调用,可以创建一个socket实例

/* * 参数: * domain: 通信域 * type:类型 * protocal:通信范围 * 返回值: * 如果正确,返回一个int类型的值,表示该socket实例唯一标识符的文件描述符。这个值可以调用其他系统调用来进行各种操作,比如绑定和监听端口、发送数据等 */ int socket(int domain, int type, int protocol) 
讯享网

每隔socket都必将存在一个通信域中,而通信域决定了该socket的地址格式和范围:
在这里插入图片描述
由上图可知:Linux提供的通信socket域由三个,分别代表了IPv4域、IPv6域、Unix域。这三个域的标识符都以AF_(address family)为前缀,表示地址族,这也暗示了每个域的socket的地址格式不同。

有很多不同的socket,这些socket相关的特性如下图:
在这里插入图片描述
由上表也可以看出,数据形式有两种:数字报和字节流

  • 以数据报为数据形式意味着数据接收方的socket接口程序可以意识到数据的边界并且对它们进行划分,这样就省去了接收方的应用程序寻找数据边界和切分数据的工作量
  • 以字节流为数据形式的数据传输实际上传输的是一个字节接着一个字节的串,类似字节数组。一般情况下,字节流并不能体现出哪些字节数据哪个数据包,因此,socket接口程序是无法从中分离出数据包的,这一工作只能由应用程序完成。但是SOCK_seqpacket类型的例外,数据发送方的socket接口程序可以中时的记录数据边界【每次发送的字节流片段的分界点】,这些边界信息会随着字节流一起发送给数据接收方,数据接收方的socket会根据数据边界将字节流还原成若干个字节流判断并且按照需要一次传递给应用程序。

面向连接与无连接socket:

  • 在面向有链接的socket之间传输数据之前,我们必须先建立逻辑来凝结,在连接建立之后,通信双方可以很方便的互相传递数据,并且由于连接已经暗含了对方的地址,所以在传输数据的时候不必在指定目标地址。【跟谁建立的就和谁通信,这个连接不能在和别人通信了】
  • 面向无连接的socket则不同,这类socket在通信的时候不需建立连接,它们传输的每一个数据包都是独立的,并且会直接发送到网络上,这些数据包中都含有目标地址,因此每个数据包都可能发送到不同的目的地。另外,在面向无连接的socket上,数据流只能是单向的,要么只能发送,要么只能接收。

数据传输的有序性和可靠性与socket是否面向连接有很大的关系,正是因为逻辑连接的存在,通信双方才可以通过一些手段(比如基于TCP协议的序列号和确认应答)来保证从数据发送方的数据能够及时有效正确有序的到达数据接收方,并且被接收方接受。

ps:SOCK_RAW类型的socke提供了一个可以直接通过底层(TCP/IP协议中的网络互连层)传输数据的方法。为了保证安全性,应用程序必须具有操作系统的超级用户权限才能使用这种方式。并且,应用程序需要自己建立数据传输格式【类似TCP协议的数据段格式、UDP的数据报格式】。因此,这个类型的socket很少用。

在调用socket的时候,第三个参数一般是0,表示让操作系统内核根据第一个参数和第二个1参数自己决定socket所用的协议。也就是说,socket的通信域和使用协议之间穿在对应关系,如下图:
在这里插入图片描述
注:

  • TCP、UDP、SCTP(数据报控制传输协议)都是TCP/IP协议栈中的传输层
  • TPv4、IPv6表示TPC/IP协议栈中的网络互连层协议IP的第4个版本和第6个版本
  • 有效表示该通信域和类型的组合会使内核选择某个内部的socket协议。无效表示该通信域和类型的组合不合法

基于TPC/IP协议栈的socket通信

socket接口与TCP/IP协议栈与操作系统内核的关系:
在这里插入图片描述
基于TCP/IP协议的socket通信的一个简单流程:

在这里插入图片描述
对于golang
在这里插入图片描述
第一个API:获取监听器

讯享网/* * 作用:获取监听器 * 参数: * * network:以何种协议监听给定的地址,必须是面向流的协议 * * address: 比如127.0.0.1:8080 * 返回值: * * Listener:表示监听器 * * error:错误信息 */ func Listen(network, address string) (Listener, error) 
  • network取值:
    • socket协议如下:在这里插入图片描述
    • 但是因为network必须是面向流的协议,因此network的值可以是:tcp、tcp4、tcp6、unix、unixpacket
      • tcp4表示基于IPv4的TCP协议
      • tcp4表示基于IPv4的TCP协议
      • tcp表示socket所用的tcp协议会兼容IPv4和IPv6
      • unix和unixpacket表示两个通信域为Unix域的内部socket协议,遵循它们的socket实例仅用于本地计算机不同应用程序之间的通信
  • address的取值:
    • 格式:host:port、:port
    • API会通过DNS找到与address对应的机器,如果host没有在DSN中注册,会返回error
  • 调用示例:
listen, err := net.Listen("tcp", ":8888") 

第2个API:等待客户端连接

讯享网// 当调用监听器的Accept()方法时,流程会被阻塞,直到某个客户端程序与应用程序建立TCP连接。 /* * 返回值: conn,表示当前TCP连接的net.Conn类型值 */ conn, err := listen.Accept() // 创建用户数据通信的socket 

第3个API:这是一个客户端API

/* * 作用:向指定的网络地址发送连接建立申请 * 参数: * * network:类似第一个API,只是取值更加广泛(因为在发送数据之前不一定要先建立连接) * * address:网络地址,类似 192.168.1.11:8888 * 返回值: * * Conn * * err: */ func Dial(network, address string) (Conn, error) 
  • network取值: tcp、tcp4、tcp6、unix、unixpacket、udp、udp4、udp6、ip、ip4、ip6、unixpgram
    • udp、udp4、udp6类似tcp、tcp4、tcp6,只是采用UDP协议
    • unixpgram类似unix、unixpacket,也代表了一种基于Unix域的内部协议,和它们不同的是,unixpgram是以数据报作为传输协议的。
  • error:
    • 当network是双向协议,如果远程地址没有正在监听的程序,会返回一个非nil的error
    • 由于网络中存在延迟现象,因此在收到另一方的有效回应【成功/失败】,客户端会阻塞等待一段时间。在超过等待时间之后,会返回一个非nil的error。 不同操作系统对不同协议的连接请求的超时时间不同。当然,这个超时时间也可以改,相应API见第4个API
      • linux中,TCP协议的超时时间是75s
  • 疑问:客户端的地址在哪里给出???
    • 回答:不用给出。客户端使用的端口号可以由应用程序指定,也可以由操作系统内核动态分配。
    • 注:地址中的host部分由操作系统内核指定

在这里插入图片描述

  • 调用示例:
 conn, err := net.Dial("tcp", "127.0.0.1:8888") 

第4个API:这是一个客户端API

/* * 作用:向指定的网络地址发送连接建立申请 * 参数: * * network:类似第一个API,只是取值更加广泛(因为在发送数据之前不一定要先建立连接) * * address:网络地址,类似 192.168.1.11:8888 * * timeout超时时间:单位是纳秒 * 返回值: * * Conn * * err: */ func DialTimeout(network, address string, timeout time.Duration) (Conn, error) 

调用示例:

net.DialTimeout("tcp", "127.0.0.1:8888", 2 * time.Second) 

上面的4个API已经足够在服务端程序和客户端程序之间建立TCP连接了【里面封装了一些底层操作,比如创建socket实例、绑定地址机地址等】

在创建监听器并且开始等待连接请求之后,一旦收到客户端的连接请求,服务端就会与客户端建立TCP连接(三次握手)。这个连接的建立过程是由两端的操作系统内核共同协调完成的。当成功建立连接之后,不管是服务器还是客户端,都会得到一个net.Conn类型的值,此后,通信两端就可以分别利用各自的net.Conn类型值交换数据了。接下来,我们来探讨golang API在net.Conn类型之上提供的功能。

  • Go的socket编程API程序在底层获取的是一个非阻塞式的socket实例,这表示建立在该实例上的数据读取操作也是非阻塞式的,在应用程序试图通过系统调用read从socket的接收缓冲区中读取数据时,即使该接受缓冲区中没有数据,操作系统内核也不会使系统调用read进入阻塞状态,而是直接返回一个错误码为EAGAIN(eagain)的错误。但是应用程序并不应该把它看成是一个真正的错误,而是忽略它,然后稍等片刻再去尝试读取。如果在读取数据的时候接收缓冲区由数据,那么系统调用read就会携带这些数据立即返回。即使当前的接收缓冲区中只包含了一个字节的数据,这一特性叫做部分读。
  • 在应用程序试图向socket的发送缓冲区写入一段数据的时候,即使发送缓冲区已经满了,系统调用的write也不不会阻塞,而是直接返回一个错误码EAGAIN(eagain)的错误。同样应用程序会忽略这个错误并且稍后再尝试写入数据,如果发送缓冲区中由少许剩余空间但是不足以放入这段数据,那么系统调用write会尽可能写入一部分数据然后返回已写入的字节的数据量,这一部分叫做部分写。应用程序应该再每次调用write之后都会去检查该结果值,并在发现数据没有被完全写入的时候继续写入剩下的数据。
  • 系统的accept也是非阻塞的,他不会阻塞等待新连接的到来,而是直接返回一个错误码为EAGAIN(eagain)的错误。

这可能和我们之前说的不一样,别着急,原因如下:

Go的socket编程API我们屏蔽了相关系统的EAGAIN(eagain)的错误,使得有些socket编程API调用起来像非阻塞的。但是我们要明确,底层使用的是非阻塞式的socket接口

  • 非阻塞socket接口屏蔽了部分写特性:
    • 在不发生错误的情况下,相关API直到把所有数据全部写入到socket的发送缓冲区之后才会返回
  • 非阻塞socket接口保留了部分读特性,这个有调用方自己处理:
    • 因为TCP协议上传输的数据是字节流、数据发无法感知数据的边界,所以socket编程API无法判断函数需要返回的时机,因此,把数据切分和分批返回的任务交给了调用方

好了,现在我们看net.Conn类型,它是一个接口类型,定义了可以在一个连接上做的所有事情:

# Conn是一种通用的面向流的网络连接: type Conn interface { 
    Read(b []byte) (n int, err error) Write(b []byte) (n int, err error) Close() error LocalAddr() Addr RemoteAddr() Addr SetDeadline(t time.Time) error SetReadDeadline(t time.Time) error SetWriteDeadline(t time.Time) error } 

接下来我们一起探讨下它们的方法吧。

第1个方法:

/* * * 作用:用于从socket的接收缓冲区中读取数据 * * 参数: * * b:用于存放从连接上接收数据的容器,长度由应用程序决定。 * * 返回值: * * n:本次操作实际读取到的数据 */ Read(b []byte) (n int, err error) 
  • b: 用来接收另一方传递给conn的数据
    • Read方法会把它当成空的容器并且试图填满,该容器中相应位置上的原元素值会被替换,我们应该保证这个容器在填充之前保持绝对干净,也就是说,传递给Read方法的参数值应该是一个不包含任何非零元素的切片值。
    • 一般情况下:Read方法只有把参数值填满才返回,但是有时会b没有填满就会返回,如果b的靠后部分有遗留元素的话,就会混乱,这个时候我们可以使用他的返回值n,用来帮助我们从中识别出真正的数据部分
    • 部分读特性:如果发送的数据大于buff长度,因为Read把buff填充满了就会返回。此时还没有被读取的数据就会留在socket中。如果下次试图读取,就会先读到上次残留的数据
  • n:就是本次操作中Read方法向参数值中填充的字节个数
  • err: 如果从socket的接收缓冲区读取数据的时候发现另一端的TCP已经关闭,将会返回一个error

  • 使用示例:循环读取数据【不进行数据切割等操作】并追加到bytes.Buffer,直到另一端关闭,才把数据打印出来
func process(conn net.Conn) { 
    defer conn.Close() var dataBuffer bytes.Buffer buf := make([]byte, 1024) for { 
    n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容 if(err != nil){ 
    if err == io.EOF{ 
    fmt.Printf("客户端退出 err=%v\n", err) }else{ 
    fmt.Printf("read err=%v\n", err) } break } dataBuffer.Write(buf[:n]) } fmt.Printf(dataBuffer.String()) } 
  • 网络IO的优化: bufio.NewReader(conn)

第2个方法:

/* * * 作用:用于从socket的接收缓冲区中写数据 * * 参数: * * b:用于存放从连接上接收数据的容器,长度由应用程序决定。 * * 返回值: * * n:本次操作实际读取到的数据 */ Write(b []byte) (n int, err error) 

第3个方法:

/* * 作用:关闭当前的连接 */ Close() error 
  • 关闭当前的连接。当连接被关闭之后,不管是不是还有Read等正在阻塞中,都会立即返回一个错误结束。

第4、5个方法:

/* * 作用:返回本机地址, 如果没有指定,那么操作系统内核会自动分配一个 */ LocalAddr() Addr /* * 作用:返回远程地址, 如果没有指定,那么操作系统内核会自动分配一个 */ RemoteAddr() Addr type Addr interface { 
    Network() string // name of the network (for example, "tcp", "udp") String() string // string form of address (for example, "192.0.2.1:25", "[2001:db8::1]:80") } 

第6个方法:

/* * * 作用:设定在当前连接上的读操作的超时时间 * * 参数: * t time.Time: * * 返回值: * error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。 */ SetReadDeadline(t time.Time) error 

第7个方法

/* * * 作用:设定在当前连接上的写操作的超时时间 * * 参数: * t time.Time: * * 返回值: * error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。 */ SetWriteDeadline(t time.Time) error 
  • 注: 写操作超时不一定写操作完全没有成功,因为有可能有部分数据写入到了socket的发送缓冲区了。

第8个方法:

/* * * 作用:设定在当前连接上的IO操作的超时时间 * * 参数: * t time.Time: * * 返回值: * error: 如果超时时间到了操作还没有完成,就是error,提示i/o timeout。 */ SetDeadline(t time.Time) error 
  • t :
    • 是绝对时间,对之后的每个IO操作都有效,相当于SetReadDeadline + SetWriteDeadline
    • 如果不再需要设置超时事件里,就要及时取消它。也就是SetDeadline的参数设置为time.Time的0值。由于time.Time是一个结构体类型,所以可以用time.Time{}来表示它的零值。也就是说取消超时时间的代码为conn.SetDeadline(time.Time{})
  • 使用示例:
 // 在 主go程中, 获取服务器回发数据。 buf2 := make([]byte, 4096) for { 
    conn.SetDeadline(time.Now().Add(2*time.Second)) n, err := conn.Read(buf2) ***** } 
  • 第6、7、8个方法主要用于针对短连接。关于短连接与长连接请参考下面

对长短连接的处理

对于服务端,Server可能会和很多Client进行通信交互,所以我们必须保证整个Server运行状态的稳定性,因此在和Client建立连接通信的时候,确保连接的及时断开非常重要,否则一旦和多个客户端建立不关闭的长连接,对于服务器端资源的占用是非常可怕的

  • 前置知识
  • Socket的长连接和短连接

短连接超时

针对短连接,我们可以使用golang中的net包自带的timeout函数:

func (*IPConn) SetDeadline func (c *IPConn) SetDeadline(t time.Time) error func (*IPConn) SetReadDeadline func (c *IPConn) SetReadDeadline(t time.Time) error func (*IPConn) SetWriteDeadline func (c *IPConn) SetWriteDeadline(t time.Time) error 

如果我们想要给服务端设置短连接的timeout,如下:

conn, err := listen.Accept() // 创建用户数据通信的socket if err != nil { 
    fmt.Println("Accept() err=", err) conntine } else { 
    fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String()) } conn.SetDeadline(time.Now().Add(time.Duration(10) * time.Second)) 

通过这样设定,每个和Server通讯的Client连接时长最长也不会超过10s了

长连接心跳

作为长连接,由于我们往往很难确定什么时候会中断连接,因此并不能像处理短连接那样简单粗暴的设定一个timeout就可以搞定,而在Golang的net包中,并没有针对长连接的函数,因此需要我们自己设计并实现针对长连接的处理策略啦~

针对socke长连接,常见的做法是在Server和Socket之间设计通讯机制,当两者之间没有信息交互时,双方便会定时发送数据包(心跳),以维持连接状态。

这种方法是目前使用相对比较多的做法,但是开销相对也较大,特别是当Server和多个client保持长连接的时候,并发会比较高,因此还有一种策略,逻辑相对简单,开销相对较小:

当server每次收到clinet发来的消息之后,就开始心跳计时,如果在心跳计时结束之前没有再次收到Client发来的信息,那么就会断开跟Client的连接。而一旦在设计时间再次收到Client发来的信息,那么Server便会重置计时器,再次重新进行心跳计时,直到超时断开连接为止

客户端:

package main import ( "fmt" "net" ) func main(){ 
    tcpaddr,err:=net.ResolveTCPAddr("tcp4","127.0.0.1:8888") if err!=nil{ 
    panic(err) } conn,err:=net.DialTCP("tcp",nil,tcpaddr)//链接 if err!=nil{ 
    panic(err) } for{ 
    fmt.Println("阻塞等待写入数据:") var inpustr string fmt.Scanln(&inpustr) conn.Write([]byte(inpustr)) /* fmt.Println("阻塞读取数据:") buf:=make([]byte,1024) n,_:=conn.Read(buf)//读取数据 fmt.Println(string(buf[:n]))*/ } } 

服务端:

package main import ( "fmt" "log" "net" "time" ) //判断30秒内有没有产生通信 //超过30秒退出 func HeartBeat(conn net.Conn,heartchan chan byte,timeout int){ 
    fmt.Println(" HeartBeat start。。。。。") select { 
    // 发现只要有一个case执行了,该select就会退出【不确定是不是理解的对了】 case hc:=<-heartchan: log.Println("read chan》",string(hc)) conn.SetDeadline(time.Now().Add(time.Duration(timeout)*time.Second)) // conn会自己接收 fmt.Println("conn deadline set finish") /* case <-time.After(time.Duration(timeout)*time.Second) : // 这一段是没有用的 fmt.Println("kkkkk") log.Println(conn.RemoteAddr(), "time out. will close conn")//客户端超时 conn.Close() fmt.Println("ddddddddd")*/ } fmt.Println(" HeartBeat end。。。。。") } //处理心跳的channel func HeartChanHandler(n[]byte,beatch chan byte){ 
    fmt.Println(" HeartChanHandler",len(n)) for _,v:=range n{ 
    fmt.Println("put *> chan", string(v)) beatch<-v } close(beatch)//关闭管道 //fmt.Println(" HeartChanHandler end, close chan") } func MsgHandler(conn net.Conn){ 
    //flag := true buf :=make([]byte,1024) defer conn.Close() for { 
    fmt.Println("阻塞等待数据读取:") n,err:=conn.Read(buf) if err!=nil{ 
    fmt.Println("when read conn, conn closed") //flag = false break } msg:=buf[:n] /* * do something */ //conn.Write([]byte("收到数据:"+string(buf[1:n])+"\n")) fmt.Println("收到数据: ", string(buf)) beatch:=make(chan byte) // beatch没有缓冲区 go HeartBeat(conn,beatch,30) go HeartChanHandler(msg[:1],beatch) // beatch中的数据必须要及时读取完,否则会一直阻塞。导致该协程不能推出, 因此只msg的第一个数组作为心跳 } fmt.Println("当前处理数据的协程结束。。。。") } func main(){ 
    listener,err:=net.Listen("tcp","127.0.0.1:8888") if err!=nil{ 
    panic(err)//处理错误 } defer listener.Close()//延迟关闭 for{ 
    new_conn,err:=listener.Accept()//接收消息 if err!=nil{ 
    panic(err)//处理错误 } go MsgHandler(new_conn)//处理客户端消息 } } 

在这里插入图片描述

参考

并发服务端

服务端的处理流程

  • 监听端口:8888
  • 接受客户端的tcp连接,建立客户端和服务端的连接
  • 创建goroutine,处理该链接的请求

客户端的处理流程

  • 建立与服务端的连接
  • 发送请求数据,接收服务器返回的结果数据
  • 关闭连接

服务端:

package main import ( "bytes" "fmt" "net" "runtime" "strconv" ) func main() { 
    listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket if err != nil { 
    fmt.Println("listen err=", err) return } fmt.Println("监听套接字,创建成功, 服务器开始监听。。。") defer listen.Close() // 服务器结束前关闭 listener // 循环等待客户端来链接 for { 
    fmt.Println("阻塞等待客户端来链接...") conn, err := listen.Accept() // 创建用户数据通信的socket if err != nil { 
    fmt.Println("Accept() err=", err) } else { 
    fmt.Println("通信套接字,创建成功。。。") fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String()) } // 这里准备起一个协程,为客户端服务 go process(conn) } } func process(conn net.Conn) { 
    defer conn.Close() for { 
    // 创建一个新切片, 用作保存数据的缓冲区 buf := make([]byte, 1024) fmt.Printf("服务器在等待客户端%s 发送信息, 当前线程 %v\n", conn.RemoteAddr().String(), goID()) n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容 if err != nil { 
    fmt.Printf("客户端退出 err=%v\n", err) return } fmt.Printf("当前线程 %v, 接受消息 %s\n", goID(), string(buf[:n])) // 回写数据给客户端 /*_, err = conn.Write([]byte("This is Server\n")) if err != nil { fmt.Println("Write err:", err) return } */ } } func goID() uint64 { 
    b := make([]byte, 64) b = b[:runtime.Stack(b, false)] b = bytes.TrimPrefix(b, []byte("goroutine ")) b = b[:bytes.IndexByte(b, ' ')] n, _ := strconv.ParseUint(string(b), 10, 64) return n } 

客户端:

package main import ( "bufio" "fmt" "net" "os" "strings" ) func main() { 
    fmt.Println("建立与服务端的链接") conn, err := net.Dial("tcp", "127.0.0.1:8888") //创建用于通信socke if err != nil { 
    fmt.Println("client dial err=", err) return } defer conn.Close() reader := bufio.NewReader(os.Stdin) for { 
    fmt.Println("阻塞等待终端输入数据") line, err := reader.ReadString('\n') if err != nil { 
    fmt.Println("readString err=", err) } fmt.Println("分析终端输入的数据") line = strings.Trim(line, " \r\n") if line == "exit" { 
    fmt.Println("客户端退出") return } fmt.Println("将line发送给服务器") _, err = conn.Write([]byte(line + "\n")) if err != nil { 
    fmt.Println("conn.Write err=", err) } } } 

laifdeswa
在这里插入图片描述
在这里插入图片描述

  • Accept()函数的作用是等待客户端的链接,如果客户端没有链接,该方法会阻塞。如果有客户端链接,那么该方法返回一个Socket负责与客户端进行通信。所以,每来一个客户端,该方法就返回一个Socket与其通信.
  • 这里是并发服务器的关键点: 实现并发处理多个客户端数据的服务器,需要针对每一个客户端的连接,单独产生一个socket,并创建一个单独的goroutine与之通信。
  • 如果服务端关闭,那么与之建立连接的客户端conn也会关闭,如果客户端关闭,与之连接的那个conn关闭【只会影响到那一个】

ps:也不一定要写一个客户端才能连接,可以直接通过telnet命令测试:
在这里插入图片描述

并发Client:

服务端

package main import ( "bytes" "fmt" "net" "runtime" "strconv" ) func main() { 
    listen, err := net.Listen("tcp", ":8888") // 创建用于监听的 socket if err != nil { 
    fmt.Println("listen err=", err) return } fmt.Println("监听套接字,创建成功, 服务器开始监听。。。") defer listen.Close() // 服务器结束前关闭 listener // 循环等待客户端来链接 for { 
    fmt.Println("阻塞等待客户端来链接...") conn, err := listen.Accept() // 创建用户数据通信的socket if err != nil { 
    fmt.Println("Accept() err=", err) } else { 
    fmt.Println("通信套接字,创建成功。。。") fmt.Printf("主线程 %v 创建一个conn, Accept() suc con=%v 客户端 ip=%v\n", goID(), conn, conn.RemoteAddr().String()) } // 这里准备起一个协程,为客户端服务 go process(conn) } } func process(conn net.Conn) { 
    defer conn.Close() for { 
    // 创建一个新切片, 用作保存数据的缓冲区 buf := make([]byte, 1024) fmt.Printf("服务器在等待客户端%s 发送信息, 当前线程 %v\n", conn.RemoteAddr().String(), goID()) n, err := conn.Read(buf) // 从conn中读取客户端发送的数据内容 if err != nil { 
    fmt.Printf("客户端退出 err=%v\n", err) return } fmt.Printf("当前线程 %v, 接受消息 %s\n", goID(), string(buf[:n])) // 回写数据给客户端 _, err = conn.Write([]byte("This is Server\n")) if err != nil { 
    fmt.Println("Write err:", err) return } } } func goID() uint64 { 
    b := make([]byte, 64) b = b[:runtime.Stack(b, false)] b = bytes.TrimPrefix(b, []byte("goroutine ")) b = b[:bytes.IndexByte(b, ' ')] n, _ := strconv.ParseUint(string(b), 10, 64) return n } 
  • 客户端
package main import ( "fmt" "net" "os" ) func main() { 
    fmt.Println("建立与服务端的链接") conn, err := net.Dial("tcp", "127.0.0.1:8888") if err != nil { 
    fmt.Println("client dial err=", err) return } // 读取用户的键盘输入。 go func() { 
    buf := make([]byte, 10) for { 
    // 获取键盘输入。 fmt.Scan --》 结束标记 \n 和 空格 n, err := os.Stdin.Read(buf) // buf[:n] if err != nil { 
    fmt.Println("os.Stdin.Read err:", err) return } // 直接将读到键盘输入数据,写到 socket 中,发送给服务器 conn.Write(buf[:n]) } }() // 在 主go程中, 获取服务器回发数据。 buf2 := make([]byte, 4096) for { 
    // 借助 socket 从服务器读取 数据。 n, err := conn.Read(buf2) if n == 0 { 
    fmt.Println("客户端检查到服务器,关闭连接, 本端也退出") return } if err != nil { 
    fmt.Println("os.Stdin.Read err:", err) return } fmt.Println("客户端读到:", string(buf2[:n])) } } 

在这里插入图片描述

源码分析:

conn

  • conn结构体:是一个*netFD的网络文件描述符,实现了Conn接口方法:
type conn struct { 
    fd *netFD } // netFD结构体:是网络文件描述符 type netFD struct { 
    pfd poll.FD // immutable until Close family int sotype int isConnected bool // handshake completed or use of association with peer net string laddr Addr raddr Addr } 
  1. Read 方法用于从conn对象中读取字节流并写入[]byte类型的b对象中。
func (c *conn) Read(b []byte) (int, error) { 
    if !c.ok() { 
    return 0, syscall.EINVAL } n, err := c.fd.Read(b) if err != nil && err != io.EOF { 
    err = &OpError{ 
   Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err } func (fd *netFD) Read(p []byte) (n int, err error) { 
    if err := fd.readLock(); err != nil { 
    return 0, err } defer fd.readUnlock() if err := fd.pd.PrepareRead(); err != nil { 
    return 0, &OpError{ 
   "read", fd.net, fd.raddr, err} } for { 
    n, err = syscall.Read(int(fd.sysfd), p) if err != nil { 
    n = 0 if err == syscall.EAGAIN { 
    if err = fd.pd.WaitRead(); err == nil { 
    continue } } } err = chkReadErr(n, err, fd) break } if err != nil && err != io.EOF { 
    err = &OpError{ 
   "read", fd.net, fd.raddr, err} } return } 
  1. Write()方法用于把[]byte类型的切片中的数据写入到conn对象中
func (c *conn) Write(b []byte) (int, error) { 
    if !c.ok() { 
    return 0, syscall.EINVAL } n, err := c.fd.Write(b) if err != nil { 
    err = &OpError{ 
   Op: "write", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return n, err } 
  1. Close()方法用于关闭conn连接
func (c *conn) Close() error { 
    if !c.ok() { 
    return syscall.EINVAL } err := c.fd.Close() if err != nil { 
    err = &OpError{ 
   Op: "close", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } return err } func (c *conn) ok() bool { 
    return c != nil && c.fd != nil } 

laiyuan

小讯
上一篇 2025-03-28 08:14
下一篇 2025-01-18 16:45

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/63925.html