当前位置:   article > 正文

Golang学习(三十四) tcp编程_golang tcp

golang tcp

Golang的主要设计目标之一,就是面向大规模后端服务程序

网络通讯这块是服务端程序,必不可少也是至关重要的的

网络编程

网络编程主要分为tcp和http编程,如下

  1. 1、TCP socket编程 //是网络编程的主流, 之所以叫TCP socket编程
  2. //是因为底层是基于TCP/IP协议的,比如QQ
  3. 2、http编程 //(b/s)模式 浏览器去访问服务器时使用的就是http协议
  4. //而http底层依旧是用的tcp socket实现的 比如京东,属于go web开发

举个栗子

说明

  1. 1. 只要是做服务程序,都必须监听一个端口
  2. 2. 该端口就是其他程序和该服务通讯的通道
  3. 3. 一台电脑有65535个端口 1-66535
  4. 4. 一旦一个端口被某个程序监听(占用), 那么其他的程序就不能在该端口的监听

一、socket编程流程了解

为了方便,我们将tcp socket编程,简称 socket编程

 服务端流程处理

  1. 1、 监听端口
  2. 2、 接收客户端的tcp链接,建立客户端和服务端的链接
  3. 3、 创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)

客户端流程处理

  1. 1、 建立与服务端的链接(socket)
  2. 2、 发送请求数据(终端), 接收服务器端返回的结果数据
  3. 3、 关闭链接

通讯流程图

服务端监听端口8888,客户端链接8888端口后,建立链接

服务端主线程(P) 接收到客户端链接时,开启一个协程

处理客户端的请求,使得我们可以做一些分支来处理请求

 二、socket编程包和函数

使用go做socket网络开发使用的是 net 包,前往官方文档

https://studygolang.com/pkgdoc

开头的文档中说明了可以使用Dial、Listen和Accept 这三函数就能提供基本的接口

 先来看看这3个函数是做什么的 ,如下

1、Dial

在网络上连接地址,并返回一个Conn接口,可以看出他是 用来连接服务器用

 2、Listen

在本地网络地址上监听的Listener,说明他是用来监听当前主机的,再看一下Listener类型

 可以看到Listener结构体下有3个函数,我们这里先看Accept,等待下一个连接

三、socket编程快速入门

们上面找到了3个函数,现在用这3个函数我们做一个小案例

 1、添加服务端监听

func Listen(net, laddr string)(Listener, error)

根据语法我们得知要传入两个参数(协议,监听地址+端口)

案例

vi server.go

  1. package main
  2. import(
  3. "fmt"
  4. "net"
  5. )
  6. func main(){
  7. fmt.Println("服务器开始监听了")
  8. listen,err := net.Listen("tcp","0.0.0.0:8888")
  9. //我们指定了协议为tcp,那么其他人来通讯必须也是tcp
  10. //127.0.0.1只支出ipv4访问
  11. //0.0.0.0 支持ipv4和ipv6
  12. if err != nil { //根据返回值判断监听是否成功
  13. fmt.Println("listen err=",err)
  14. return
  15. }
  16. fmt.Println(listen)
  17. }

我们发现这个程序就跑了一下就停止了,我们给他一个循环并提供一个连接接收请求

  1. package main
  2. import(
  3. "fmt"
  4. "net"
  5. )
  6. func main(){
  7. fmt.Println("服务器开始监听了")
  8. listen,err := net.Listen("tcp","0.0.0.0:8888")
  9. if err != nil {
  10. fmt.Println("listen err=",err)
  11. return
  12. }
  13. defer listen.Close() //Listen 函数返回的是Listener结构体
  14. //该结构体下我们刚才看了,有一个Close的关闭连接的方法
  15. //上面说了,服务器一共有65536个端口,每个客户端请求都会占用一个随机端口
  16. //这里当请求接收后就断掉请求
  17. for{ //创建for循环让程序不要直接退出
  18. fmt.Println("等待客户端链接")
  19. conn,err := listen.Accept() //使用Accept方法,等待用户连接,如果没有请求就一直卡在这里
  20. //当请求结束后,进入下一个循环再次等待
  21. if err != nil{
  22. fmt.Println("Accept() err",err) //如果连接失败了我们为了不影响其他线程
  23. //不使用return退出for循环
  24. }else {
  25. fmt.Println("链接成功了",conn)
  26. }
  27. }
  28. }

 返回

  1. 服务器开始监听了
  2. 等待客户端链接 //卡在Accept的位置等待客户端请求

 测试

telnet 127.0.0.1 8888

 返回

  1. 服务器开始监听了
  2. 等待客户端链接
  3. 链接成功了 &{{0xc00007ac80}}
  4. 等待客户端链接

可以看到已经链接请求成功了,并且因为for循环还会卡在Accept处等待请求

2、编写客户端程序

上面我们知道通过Dial 函数可以与服务端建立连接,格式如下

func Dial(network, address string) (Conn, error)

案例

vi client.go

  1. package main
  2. import(
  3. "fmt"
  4. "net"
  5. )
  6. func main(){
  7. conn,err := net.Dial("tcp","127.0.0.1:8888") //连服务端的协议,和服务端一致
  8. //连接服务端的地址和端口,本机
  9. if err != nil{
  10. fmt.Println("链接失败")
  11. return
  12. }
  13. fmt.Println("链接服务端成功",conn)
  14. }

 返回

链接服务端成功 &{{0xc00007aa00}}

Dial 函数返回的是一个Conn的接口,我们来看这个接口下有什么东西

 

 客户端连接后,客户端和服务端都会得到一个相同的Conn 接口

 3、服务端获取客户端信息

在Conn这个请求连接的结构体中,有一个RemoteAddr() Addr 的方法

他主要的作用就是获取整个连接下的一个读写权限,先看返回值Addr是什么

 

修改服务端

vi server.go

  1. package main
  2. import(
  3. "fmt"
  4. "net"
  5. )
  6. func main(){
  7. fmt.Println("服务器开始监听了")
  8. listen,err := net.Listen("tcp","0.0.0.0:8888")
  9. if err != nil {
  10. fmt.Println("listen err=",err)
  11. return
  12. }
  13. defer listen.Close()
  14. for{
  15. fmt.Println("等待客户端链接")
  16. conn,err := listen.Accept()
  17. if err != nil{
  18. fmt.Println("Accept() err",err)
  19. }else {
  20. fmt.Printf("链接成功了=%v ipadd=%v\n",conn,conn.RemoteAddr().String()) //RemoteAddr() 客户端信息,string转字符串
  21. fmt.Println(conn.RemoteAddr().Network())
  22. }
  23. }
  24. }

运行服务端 客户端测试

  1. 服务器开始监听了
  2. 等待客户端链接
  3. 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:5448
  4. 等待客户端链接

可以看到,我们就拿到了客户端的ip以及端口

四、常用案例

1、客户端提交数据给服务端

客户端提交数据,我们这里要让用户从终端去输入文本内容

通过bufio.NewReader 方法去建立缓存,并指定os.Stdin去读取标准输入

  1. func Input() string {
  2. reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
  3. line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
  4. if err != nil {
  5. fmt.Println("readString err=",err)
  6. }
  7. return line
  8. }

 这样我们就可以读取到客户端在终端输入的信息了,下面我们要将接收的数据发送给服务端

之前我们客户端连接服务端的时候,建立连接返回了一个Conn的接口,我们查看一下

 我们可以把Conn看做是一个传输带,通过Write可以将输入进行写入

然后再通过Read去读取写入的数据,这里我们读取写入的类型是一个切片

返回值中有个int和error,这里的int是指切片的长度,方便我们取出切片所有数据

 案例

vi client.go

  1. package main
  2. import(
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. )
  8. func Input() string {
  9. reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
  10. line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
  11. if err != nil {
  12. fmt.Println("readString err=",err)
  13. }
  14. return line
  15. }
  16. func main(){
  17. conn,err := net.Dial("tcp","127.0.0.1:8888")
  18. if err != nil{
  19. fmt.Println("链接失败")
  20. return
  21. }
  22. line := Input()
  23. n,err := conn.Write([]byte(line)) //返回切片的总长度
  24. if err != nil{
  25. fmt.Println("conn.Write err=",err)
  26. }
  27. fmt.Printf("客户端发送了 %d 字节的数据,并退出",n)
  28. }

测试

  1. //服务端运行
  2. //客户端运行
  3. asdasd //手动输入数据

返回

  1. 客户端发送了 7 字节的数据,并退出
  2. 进程 已完成,退出代码为 0

我们客户端向服务端发送了一个数据,但还没有接收,这样是没有意义的,写下接收代码

 2、服务端接收客户端发送的数据

上面说了,我们可以使用conn接口中的Read去读取客户端写入的数据

 vi server.go

  1. package main
  2. import(
  3. "fmt"
  4. "net"
  5. )
  6. func process(conn net.Conn){ //处理客户端请求函数
  7. defer conn.Close() //表示当前传进来的这个客户端连接只要下面逻辑处理完了
  8. //那么就直接结束掉这个连接,如果还要处理就再次连接
  9. for {
  10. buf := make([]byte,1024) //我们前面知道,客户端write写入的数据是一个 []byte类型的数据
  11. //但是我们不清楚他具体要写多大的数据,所以这里空间大小先设置为1024
  12. fmt.Println("服务器在等待客户端发送信息\n", conn.RemoteAddr().String())
  13. n,err := conn.Read(buf) //通过Read方法,去接收客户端发送的数据
  14. //这里指定了buf的切片, 意思是把客户端发送的数据存放到buf变量中
  15. //Read会等待客户端通过conn发送信息
  16. //如果客户端没有发送write动作,那么这个协程就会阻塞在这里等待
  17. if err != nil{
  18. fmt.Println("服务器端的read err=",err)
  19. return //当客户端完成任务或异常关闭后,这边我们就将协程退出,否则会循环报错链接
  20. }
  21. fmt.Print(string(buf[:n])) //打印输出
  22. //这里的Print不用带ln,因为过来的数据是带换行的
  23. }
  24. }
  25. func main(){
  26. fmt.Println("服务器开始监听了")
  27. listen,err := net.Listen("tcp","0.0.0.0:8888")
  28. if err != nil {
  29. fmt.Println("listen err=",err)
  30. return
  31. }
  32. defer listen.Close()
  33. for{
  34. fmt.Println("等待客户端链接")
  35. conn,err := listen.Accept()
  36. if err != nil{
  37. fmt.Println("Accept() err",err)
  38. }else {
  39. fmt.Printf("链接成功了=%v ipadd=%v",conn,conn.RemoteAddr().String())
  40. go process(conn) //当客户端连接成功了,我们开启一个协程去处理这个连接
  41. }
  42. }
  43. }

测试

运行服务端、客户端  在客户端输入文本信息,查看服务端返回信息

客户端输入

  1. 123
  2. 4
  3. 客户端发送了 4 字节的数据,并退出
  4. 进程 已完成,退出代码为 0

 服务端信息

  1. 服务器开始监听了
  2. 等待客户端链接
  3. 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:12659等待客户端链接
  4. 服务器在等待客户端发送信息
  5. 127.0.0.1:12659
  6. 123
  7. 服务器在等待客户端发送信息
  8. 127.0.0.1:12659
  9. 服务器端的read err= read tcp 127.0.0.1:8888->127.0.0.1:12659: wsarecv: An existing connection was forcibly closed by the remote host.

可以看到,在服务端接收并输出第一次数据后,再次等待客户端输入

但是客户端已经退出了,服务端发现客户端断线,这个协程就也退出了

 

3、客户端自定义是否退出

上面的程序太不友好了,比如我们玩游戏,玩一次就直接挂断了吗

我们要给他添加一个机制,只有我们输入exit时才进行退出

vi client.go

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. "strings"
  8. )
  9. func Input(conn net.Conn) string {
  10. reader := bufio.NewReader(os.Stdin)
  11. line,err := reader.ReadString('\n')
  12. if err != nil {
  13. fmt.Println("readString err=",err)
  14. }
  15. line = strings.Trim(line,"\r\n") //我们客户端输入完成后会按回车键
  16. //这样会携带一个\n
  17. //有时候我们不想要这个\n 可以这么去除
  18. if line == "exit"{ //如果用户输入的是exit就退出
  19. return "exit"
  20. }
  21. _,err = conn.Write([]byte(line + "\n")) //上面我们已经去除了\n 这里为了显示好看再加上,或者去服务端加也行
  22. if err != nil{
  23. fmt.Println("conn.Write err=",err)
  24. }
  25. return line
  26. }
  27. func main(){
  28. conn,err := net.Dial("tcp","127.0.0.1:8888")
  29. if err != nil{
  30. fmt.Println("链接失败")
  31. return
  32. }
  33. for{
  34. line := Input(conn)
  35. if line == "exit"{ //如果用户输入的是exit就退出
  36. fmt.Println("客户端退出")
  37. break
  38. }
  39. }
  40. }

客户端运行

  1. 123
  2. 456
  3. 789
  4. exit

服务端运行查看

  1. 服务器开始监听了
  2. 等待客户端链接
  3. 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:9855等待客户端链接
  4. 服务器在等待客户端发送信息
  5. 127.0.0.1:9855
  6. 123
  7. 服务器在等待客户端发送信息
  8. 127.0.0.1:9855
  9. 456
  10. 服务器在等待客户端发送信息
  11. 127.0.0.1:9855
  12. 789
  13. 服务器在等待客户端发送信息
  14. 127.0.0.1:9855
  15. 服务器端的read err= read tcp 127.0.0.1:8888->127.0.0.1:9855: wsarecv: An existing connection was forcibly closed by the remote host.

 可以看到服务端在接收前3次数据都是正常的,在等待下一次时发现客户端断线退出了

 如上图,我们通过协程实现了多个客户端访问互不影响,可以通过多个client测试

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/753938
推荐阅读
相关标签
  

闽ICP备14008679号