赞
踩
Golang的主要设计目标之一,就是面向大规模后端服务程序
网络通讯这块是服务端程序,必不可少也是至关重要的的
网络编程主要分为tcp和http编程,如下
- 1、TCP socket编程 //是网络编程的主流, 之所以叫TCP socket编程
- //是因为底层是基于TCP/IP协议的,比如QQ
-
- 2、http编程 //(b/s)模式 浏览器去访问服务器时使用的就是http协议
- //而http底层依旧是用的tcp socket实现的 比如京东,属于go web开发
举个栗子
说明
- 1. 只要是做服务程序,都必须监听一个端口
- 2. 该端口就是其他程序和该服务通讯的通道
- 3. 一台电脑有65535个端口 1-66535
- 4. 一旦一个端口被某个程序监听(占用), 那么其他的程序就不能在该端口的监听
为了方便,我们将tcp socket编程,简称 socket编程
- 1、 监听端口
- 2、 接收客户端的tcp链接,建立客户端和服务端的链接
- 3、 创建goroutine,处理该链接的请求(通常客户端会通过链接发送请求包)
- 1、 建立与服务端的链接(socket)
- 2、 发送请求数据(终端), 接收服务器端返回的结果数据
- 3、 关闭链接
通讯流程图
服务端监听端口8888,客户端链接8888端口后,建立链接
服务端主线程(P) 接收到客户端链接时,开启一个协程
处理客户端的请求,使得我们可以做一些分支来处理请求
使用go做socket网络开发使用的是 net 包,前往官方文档
https://studygolang.com/pkgdoc
开头的文档中说明了可以使用Dial、Listen和Accept 这三函数就能提供基本的接口
先来看看这3个函数是做什么的 ,如下
在网络上连接地址,并返回一个Conn接口,可以看出他是 用来连接服务器用
在本地网络地址上监听的Listener,说明他是用来监听当前主机的,再看一下Listener类型
可以看到Listener结构体下有3个函数,我们这里先看Accept,等待下一个连接
我们上面找到了3个函数,现在用这3个函数我们做一个小案例
func Listen(net, laddr string)(Listener, error)根据语法我们得知要传入两个参数(协议,监听地址+端口)
案例
vi server.go
- package main
- import(
- "fmt"
- "net"
- )
-
- func main(){
- fmt.Println("服务器开始监听了")
-
- listen,err := net.Listen("tcp","0.0.0.0:8888")
- //我们指定了协议为tcp,那么其他人来通讯必须也是tcp
- //127.0.0.1只支出ipv4访问
- //0.0.0.0 支持ipv4和ipv6
-
- if err != nil { //根据返回值判断监听是否成功
- fmt.Println("listen err=",err)
- return
- }
-
- fmt.Println(listen)
-
- }

我们发现这个程序就跑了一下就停止了,我们给他一个循环并提供一个连接接收请求
- package main
- import(
- "fmt"
- "net"
- )
-
- func main(){
- fmt.Println("服务器开始监听了")
- listen,err := net.Listen("tcp","0.0.0.0:8888")
- if err != nil {
- fmt.Println("listen err=",err)
- return
- }
-
- defer listen.Close() //Listen 函数返回的是Listener结构体
- //该结构体下我们刚才看了,有一个Close的关闭连接的方法
- //上面说了,服务器一共有65536个端口,每个客户端请求都会占用一个随机端口
- //这里当请求接收后就断掉请求
-
-
- for{ //创建for循环让程序不要直接退出
- fmt.Println("等待客户端链接")
- conn,err := listen.Accept() //使用Accept方法,等待用户连接,如果没有请求就一直卡在这里
- //当请求结束后,进入下一个循环再次等待
- if err != nil{
- fmt.Println("Accept() err",err) //如果连接失败了我们为了不影响其他线程
- //不使用return退出for循环
- }else {
- fmt.Println("链接成功了",conn)
- }
- }
- }

返回
- 服务器开始监听了
- 等待客户端链接 //卡在Accept的位置等待客户端请求
测试
telnet 127.0.0.1 8888
返回
- 服务器开始监听了
- 等待客户端链接
- 链接成功了 &{{0xc00007ac80}}
- 等待客户端链接
可以看到已经链接请求成功了,并且因为for循环还会卡在Accept处等待请求
上面我们知道通过Dial 函数可以与服务端建立连接,格式如下
func Dial(network, address string) (Conn, error)
案例
vi client.go
- package main
- import(
- "fmt"
- "net"
- )
-
- func main(){
- conn,err := net.Dial("tcp","127.0.0.1:8888") //连服务端的协议,和服务端一致
- //连接服务端的地址和端口,本机
- if err != nil{
- fmt.Println("链接失败")
- return
- }
- fmt.Println("链接服务端成功",conn)
-
-
- }

返回
链接服务端成功 &{{0xc00007aa00}}
Dial 函数返回的是一个Conn的接口,我们来看这个接口下有什么东西
客户端连接后,客户端和服务端都会得到一个相同的Conn 接口
在Conn这个请求连接的结构体中,有一个RemoteAddr() Addr 的方法
他主要的作用就是获取整个连接下的一个读写权限,先看返回值Addr是什么
vi server.go
- package main
- import(
- "fmt"
- "net"
- )
-
- func main(){
- fmt.Println("服务器开始监听了")
- listen,err := net.Listen("tcp","0.0.0.0:8888")
- if err != nil {
- fmt.Println("listen err=",err)
- return
- }
- defer listen.Close()
-
-
- for{
- fmt.Println("等待客户端链接")
- conn,err := listen.Accept()
- if err != nil{
- fmt.Println("Accept() err",err)
- }else {
- fmt.Printf("链接成功了=%v ipadd=%v\n",conn,conn.RemoteAddr().String()) //RemoteAddr() 客户端信息,string转字符串
- fmt.Println(conn.RemoteAddr().Network())
- }
- }
- }

运行服务端 客户端测试
- 服务器开始监听了
- 等待客户端链接
- 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:5448
- 等待客户端链接
可以看到,我们就拿到了客户端的ip以及端口
客户端提交数据,我们这里要让用户从终端去输入文本内容
通过bufio.NewReader 方法去建立缓存,并指定os.Stdin去读取标准输入
- func Input() string {
- reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
-
- line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
- if err != nil {
- fmt.Println("readString err=",err)
- }
- return line
- }
这样我们就可以读取到客户端在终端输入的信息了,下面我们要将接收的数据发送给服务端
之前我们客户端连接服务端的时候,建立连接返回了一个Conn的接口,我们查看一下
我们可以把Conn看做是一个传输带,通过Write可以将输入进行写入
然后再通过Read去读取写入的数据,这里我们读取写入的类型是一个切片
返回值中有个int和error,这里的int是指切片的长度,方便我们取出切片所有数据
vi client.go
- package main
- import(
- "bufio"
- "fmt"
- "net"
- "os"
- )
-
-
- func Input() string {
- reader := bufio.NewReader(os.Stdin) //os.stdin 标准输入,接收用户终端信息
-
- line,err := reader.ReadString('\n') //读取标准输入数据,截止到\n 换行结束
- if err != nil {
- fmt.Println("readString err=",err)
- }
- return line
- }
- func main(){
- conn,err := net.Dial("tcp","127.0.0.1:8888")
- if err != nil{
- fmt.Println("链接失败")
- return
- }
-
- line := Input()
-
-
- n,err := conn.Write([]byte(line)) //返回切片的总长度
- if err != nil{
- fmt.Println("conn.Write err=",err)
- }
-
- fmt.Printf("客户端发送了 %d 字节的数据,并退出",n)
-
-
-
-
- }

测试
- //服务端运行
- //客户端运行
- asdasd //手动输入数据
返回
- 客户端发送了 7 字节的数据,并退出
- 进程 已完成,退出代码为 0
我们客户端向服务端发送了一个数据,但还没有接收,这样是没有意义的,写下接收代码
上面说了,我们可以使用conn接口中的Read去读取客户端写入的数据
vi server.go
- package main
- import(
- "fmt"
- "net"
- )
-
-
-
- func process(conn net.Conn){ //处理客户端请求函数
-
- defer conn.Close() //表示当前传进来的这个客户端连接只要下面逻辑处理完了
- //那么就直接结束掉这个连接,如果还要处理就再次连接
-
- for {
-
- buf := make([]byte,1024) //我们前面知道,客户端write写入的数据是一个 []byte类型的数据
- //但是我们不清楚他具体要写多大的数据,所以这里空间大小先设置为1024
-
- fmt.Println("服务器在等待客户端发送信息\n", conn.RemoteAddr().String())
- n,err := conn.Read(buf) //通过Read方法,去接收客户端发送的数据
- //这里指定了buf的切片, 意思是把客户端发送的数据存放到buf变量中
- //Read会等待客户端通过conn发送信息
- //如果客户端没有发送write动作,那么这个协程就会阻塞在这里等待
- if err != nil{
- fmt.Println("服务器端的read err=",err)
- return //当客户端完成任务或异常关闭后,这边我们就将协程退出,否则会循环报错链接
- }
-
-
- fmt.Print(string(buf[:n])) //打印输出
- //这里的Print不用带ln,因为过来的数据是带换行的
- }
- }
-
-
- func main(){
- fmt.Println("服务器开始监听了")
- listen,err := net.Listen("tcp","0.0.0.0:8888")
- if err != nil {
- fmt.Println("listen err=",err)
- return
- }
- defer listen.Close()
-
-
- for{
- fmt.Println("等待客户端链接")
- conn,err := listen.Accept()
- if err != nil{
- fmt.Println("Accept() err",err)
- }else {
- fmt.Printf("链接成功了=%v ipadd=%v",conn,conn.RemoteAddr().String())
- go process(conn) //当客户端连接成功了,我们开启一个协程去处理这个连接
- }
-
- }
- }

测试
运行服务端、客户端 在客户端输入文本信息,查看服务端返回信息
客户端输入
- 123
- 4
- 客户端发送了 4 字节的数据,并退出
- 进程 已完成,退出代码为 0
服务端信息
- 服务器开始监听了
- 等待客户端链接
- 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:12659等待客户端链接
- 服务器在等待客户端发送信息
- 127.0.0.1:12659
- 123
- 服务器在等待客户端发送信息
- 127.0.0.1:12659
- 服务器端的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.
可以看到,在服务端接收并输出第一次数据后,再次等待客户端输入
但是客户端已经退出了,服务端发现客户端断线,这个协程就也退出了
上面的程序太不友好了,比如我们玩游戏,玩一次就直接挂断了吗
我们要给他添加一个机制,只有我们输入exit时才进行退出
vi client.go
- package main
-
- import (
- "bufio"
- "fmt"
- "net"
- "os"
- "strings"
- )
-
-
- func Input(conn net.Conn) string {
- reader := bufio.NewReader(os.Stdin)
- line,err := reader.ReadString('\n')
- if err != nil {
- fmt.Println("readString err=",err)
- }
-
-
- line = strings.Trim(line,"\r\n") //我们客户端输入完成后会按回车键
- //这样会携带一个\n
- //有时候我们不想要这个\n 可以这么去除
- if line == "exit"{ //如果用户输入的是exit就退出
- return "exit"
- }
-
-
- _,err = conn.Write([]byte(line + "\n")) //上面我们已经去除了\n 这里为了显示好看再加上,或者去服务端加也行
- if err != nil{
- fmt.Println("conn.Write err=",err)
- }
-
- return line
- }
-
-
- func main(){
- conn,err := net.Dial("tcp","127.0.0.1:8888")
- if err != nil{
- fmt.Println("链接失败")
- return
- }
-
- for{
- line := Input(conn)
- if line == "exit"{ //如果用户输入的是exit就退出
- fmt.Println("客户端退出")
- break
- }
- }
-
- }

客户端运行
- 123
- 456
- 789
- exit
服务端运行查看
- 服务器开始监听了
- 等待客户端链接
- 链接成功了=&{{0xc00007ac80}} ipadd=127.0.0.1:9855等待客户端链接
- 服务器在等待客户端发送信息
- 127.0.0.1:9855
- 123
- 服务器在等待客户端发送信息
- 127.0.0.1:9855
- 456
- 服务器在等待客户端发送信息
- 127.0.0.1:9855
- 789
- 服务器在等待客户端发送信息
- 127.0.0.1:9855
-
- 服务器端的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测试
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。