GO网络编程(二):客户端与服务端通信【重要】

news2024/10/3 22:53:37

本节是新知识,偏应用,需要反复练习才能掌握。

目录

    • 1.C/S通信示意图
    • 2.服务端通信
    • 3.客户端通信
    • 4.通信测试
    • 5.进阶练习:客户端之间通信

1.C/S通信示意图

客户端与服务端通信的模式也称作C/S模式,流程图如下
在这里插入图片描述
其中P是协程调度器。可以看到,客户端都是通过一个共同的端口和服务端通信的,且服务端开了两个协程并发处理两个客户端。注意,协程和P之间,协程和端口之间,端口和客户端之间的链接都是双向的。

2.服务端通信

服务端功能要求如下:

(1)编写一个服务器端程序,在8888端口监听

fmt.Println("服务器开始监听....")
//net.Listen("tcp", "0.0.0.0:8888")
//1. tcp 表示使用网络协议是tcp
//2. 0.0.0.0:8888 表示在本地监听 8888端口
listen, err := net.Listen("tcp", "0.0.0.0:8888")

(2)可以和多个客户端创建链接

//循环等待客户端来链接我
for {
	//等待客户端链接
	fmt.Println("等待客户端来链接....")
	conn, err := listen.Accept()
	//这里准备其一个协程,为客户端服务
	go process(conn)
}

(3)链接成功后,客户端可以发送数据,服务器端接受数据,并显示在终端上

完整代码如下:

package main
import (
	"fmt"
	"net" //做网络socket开发时,net包含有我们需要所有的方法和函数
	_"io"
)

func process(conn net.Conn) {

	//这里我们循环的接收客户端发送的数据
	defer conn.Close() //关闭conn

	for {
		//创建一个新的切片
		buf := make([]byte, 1024)
		//conn.Read(buf)
		//1. 等待客户端通过conn发送信息
		//2. 如果客户端没有wrtie[发送],那么协程就阻塞在这里
		//fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
		n , err := conn.Read(buf) //从conn读取
		if err != nil {
			
			fmt.Printf("客户端退出 err=%v", err)
			return //!!!
		}
		//3. 显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n])) 
	}

}

func main() {

	fmt.Println("服务器开始监听....")
	//net.Listen("tcp", "0.0.0.0:8888")
	//1. tcp 表示使用网络协议是tcp
	//2. 0.0.0.0:8888 表示在本地监听 8888端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("listen err=", err)
		return 
	}
	defer listen.Close() //延时关闭listen

	//循环等待客户端来链接我
	for {
		//等待客户端链接
		fmt.Println("等待客户端来链接....")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Accept() err=", err)
			
		} else {
			fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr().String())
		}
		//这里准备其一个协程,为客户端服务
		go process(conn)
	}
	
	//fmt.Printf("listen suc=%v\n", listen)
}

流程总结:
1.使用net.Listen()初始化监听端口,把初始信息存在变量listen中
2.循环使用listen.Accept(),接收监听到的基本信息,存在变量conn中
3.每次循环用conn.Read(buf)读取客户端发送的内容,将这些操作封装到一个协程中,并发执行

3.客户端通信

客户端功能:
(1)编写一个客户端程序,能链接到服务器端的8888端口

conn, err := net.Dial("tcp", "localhost:8888")

(2)客户端可以发送单行数据,然后就退出

reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]

(3)能通过终端输入数据(输入一行发送一行),并发送给服务器端

line, err := reader.ReadString('\n')
if err != nil {
	fmt.Println("readString err=", err)
}

其中ReadString(‘\n’)表示以换行符为截止符号,读取包括换行符在内的之前的字符,所以之后还需要去除line中的换行符。
(4)在终端输入exit,表示退出程序

	line = strings.Trim(line, " \r\n")
	if line == "exit" {
		fmt.Println("客户端退出..")
		break
	}

完整代码如下:

package main
import (
	"fmt"
	"net"
	"bufio"
	"os"
	"strings"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		return 
	}
	//功能一:客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin) //os.Stdin 代表标准输入[终端]

	for {

		//从终端读取一行用户输入,并准备发送给服务器
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("readString err=", err)
		}
		//如果用户输入的是 exit就退出
		line = strings.Trim(line, " \r\n")
		if line == "exit" {
			fmt.Println("客户端退出..")
			break
		}

		//再将line 发送给 服务器
		_, err = conn.Write([]byte(line + "\n"))
		if err != nil {
			fmt.Println("conn.Write err=", err)	
		}
	}
}

注:localhost是本地ip地址,默认ipv6。
流程总结:
1.使用net.Dial()建立与服务端的链接,把初始信息存在变量conn中
2.循环使用bufio.NewReader(os.Stdin)创建缓冲区,读入用户输入的信息并存在reader中
3.使用reader.ReadString('\n')提取每行字符
4.判断输入是否为exit,若是则退出循环
4.若输入步是exit,则用conn.Write()将每行内容发送到服务端

4.通信测试

先启动服务端,内容如下:

服务器开始监听....
等待客户端来链接....

注意,如果出现类似下图的提示框请点击允许
在这里插入图片描述

再打开当前目录的命令行,启动客户端,命令行会显示如下语句

Accept() suc con=&{{0xc00008a508}} 客户端ip=[::1]:端口号

注意:这个端口号不是8888,而是客户端用来连接服务器时的本地端口。每个客户端在连接到服务器时,操作系统会为它分配一个临时的随机端口
客户端输入几行语句

D:\code\golang\尚硅谷golang\代码\chapter18\tcpdemo\client>go run "client(1).go"
123
你好

服务端内容

服务器开始监听....
等待客户端来链接....
Accept() suc con=&{{0xc00008a508}} 客户端ip=[::1]:端口号
等待客户端来链接....
123
你好

客户端输入exit即可结束

客户端退出..

D:\code\golang\尚硅谷golang\代码\chapter18\tcpdemo\client>

服务端内容

客户端退出 err=read tcp [::1]:8888->[::1]:端口号: wsarecv: An existing connection was forcibly closed by the remote host.

注意,此时服务端仍在循环监听,按ctrl+c即可结束程序。

5.进阶练习:客户端之间通信

为了使两个客户端能够相互通信,需要一个中介服务器(通常称为“中转服务器”),它负责转发每个客户端的消息给另一个客户端。服务器在两个客户端之间保持连接,接收其中一个客户端的消息,然后将其转发给另一个客户端。
实现步骤:
服务器端:
1.服务器负责接收客户端 A 和客户端 B 的连接。
2.服务器读取每个客户端发送的消息,然后将消息转发给另一个客户端。

package main

import (
	"fmt"
	"net"
	"sync"
)

var (
	clients = make(map[net.Conn]bool) // 存储连接的客户端
	mu      sync.Mutex                // 保护 clients 的并发访问
)

func main() {
	listener, err := net.Listen("tcp", ":8888")
	if err != nil {
		fmt.Println("服务器启动错误:", err)
		return
	}
	defer listener.Close()

	fmt.Println("服务器正在监听8888端口...")

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("接受连接时发生错误:", err)
			continue
		}

		// 在服务端提示有新的客户端连接
		fmt.Printf("新的客户端已连接: %v\n", conn.RemoteAddr())

		// 将客户端添加到客户端列表
		mu.Lock()
		clients[conn] = true
		mu.Unlock()

		go handleConnection(conn) // 处理每个连接
	}
}

func handleConnection(conn net.Conn) {
	defer func() {
		conn.Close()
		mu.Lock()
		delete(clients, conn) // 从列表中删除客户端
		mu.Unlock()
	}()

	buf := make([]byte, 1024)

	for {
		n, err := conn.Read(buf) // 读取客户端消息
		if err != nil {
			fmt.Printf("客户端 %v 断开连接: %v\n", conn.RemoteAddr(), err)
			return
		}

		message := string(buf[:n])

		// 判断是否是退出消息
		if message == "exit\n" || message == "exit" {
			fmt.Printf("客户端 %v 退出。\n", conn.RemoteAddr())
			return
		}

		// 转发消息给其他客户端
		mu.Lock()
		for client := range clients {
			if client != conn { // 不给发送者发送消息
				_, _ = client.Write(buf[:n]) // 将消息发送到其他客户端
			}
		}
		mu.Unlock()
	}
}

客户端:
1.客户端连接到服务器,通过服务器发送和接收消息。
2.客户端之间不会直接通信,而是通过服务器中转消息。

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	// 使用 localhost 连接到服务器
	conn, err := net.Dial("tcp", "localhost:8888")
	if err != nil {
		fmt.Println("连接服务器失败:", err)
		return
	}
	defer conn.Close() // 确保在退出时关闭连接

	// 启动一个 goroutine 负责接收服务器的消息
	go func() {
		buf := make([]byte, 1024)
		for {
			n, err := conn.Read(buf) // 从服务器读取数据
			if err != nil {
				fmt.Println("与服务器断开连接:", err)
				return
			}
			// 显示来自服务器的消息
			fmt.Print("收到消息: ", string(buf[:n]))
		}
	}()

	// 主协程负责发送用户输入的消息
	reader := bufio.NewReader(os.Stdin)

	fmt.Println("连接到服务器,输入消息发送,输入 'exit' 退出...")

	for {
		// 从终端读取一行用户输入
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("读取输入时发生错误:", err)
			continue
		}

		// 去掉输入行的换行符和空格go ru
		line = strings.TrimSpace(line)

		// 如果用户输入的是 exit,退出程序
		if line == "exit" {
			fmt.Println("客户端退出...")
			break
		}

		// 将用户输入发送给服务器
		_, err = conn.Write([]byte(line + "\n"))
		if err != nil {
			fmt.Println("发送消息时发生错误:", err)
		}
	}
}

效果截图
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2186993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Qt】控件概述——按钮类控件(2)

控件概述(2) 1. PushButton2. RadioButton——单选按钮2.1 使用2.2 区分信号 clicked,clicked(bool),pressed,released,toggled(bool)2.3 QButtonGroup分组 3. CheckBox——复选按钮 1. PushButton QPushB…

写不出论文?分享7款写论文的ai免费工具网站

在当今学术研究和写作领域,撰写高质量的论文是一项挑战性的任务。幸运的是,随着人工智能技术的发展,AI论文写作工具逐渐成为帮助学者和学生提高写作效率的重要工具。这些工具不仅能够提高写作速度,还能通过智能校对和优化&#xf…

【大数据】Doris 数据库与表操作语法实战详解

目录 一、前言 二、数据库基本操作 2.1 修改账户密码 2.2 创建新用户 2.3 创建数据库与账户授权 2.3.1 数据库创建补充说明 2.3.2 数据库账户赋权 三、数据表基本操作 3.1 Doris 数据表介绍与使用 3.1.1 建表结构说明 3.1.2 建表语法与操作 3.1.3 建表示例 - 单分区…

Android KMP 快速入门2 - Koin依赖注入

这里写目录标题 代码仓库KMP 框架基本框架actual&expectKoin 依赖注入管理 代码仓库 本小节代码已经上传到gitee,请自行查看: 点击访问仓库 KMP 框架 基本框架 源码集合描述存放内容示例androidMain针对 Android 平台的代码使用 Android SDK、Andr…

Python、C++、java阶乘算法

最近,我除了Python还学了C和Java,然后在网上看到编程考题:阶乘。 首先,我们先理解什么是阶乘。 阶乘是数学中的一个概念,通常定义为从1乘到指定的数。具体来说,一个正整数的阶乘(记作n!&#…

【课程学习】Wireless Communications

Goldsmith A. Wireless communications[M]. Cambridge university press, 2005. Wireless Communications 无线通信课程 文章目录 2-Path Loss, Shadowing, and Multipath2.4-Two-Ray Multipath Model时延扩展 delay spread P33 3-Statistical Multipath Channel Models3.3-Wid…

Python+Matplotlib创建高等数学上册P2页例3交互动画

import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import Slider from matplotlib.patches import Rectangle# 创建图形和坐标轴 fig, ax plt.subplots(figsize(12, 8)) plt.subplots_adjust(bottom0.2)# 设置坐标轴范围 ax.set_xlim(-2*np.pi, 2…

BugReport中的App Processor wakeup字段意义

一、功耗字段意义: App processor wakeup:Netd基于xt_idletimer 待机下监视网络设备的收发工作状态,即当设备发生联网从休眠态变成为唤醒态时,会记录打醒者的uid(uid大于0)和网络类型(wifi或数据类型)、时间戳 实际日志:我们在B…

【C++复习】C++11经典语法

文章目录 {}列表初始化1. 初始化内置类型变量2. 初始化数组3. 初始化标准容器4. 初始化自定义类型5. 构造函数初始化列表6. 初始化列表(initializer_list)7. 返回值初始化8. 静态成员变量和全局变量的就地初始化9. 防止类型收窄总结 decltype右值引用完美…

图像处理案例04

图像处理 问题:把不规则的图片按照参考图摆放 步骤: 1. 用ORB找关键点 2. 关键点匹配 3. 根据上一步匹配的关键点得出单应性矩阵 4. 根据单应性矩阵对不规则进行透视变换 import cv2 import numpy as np import matplotlib.pyplot as pltimgl cv2.imrea…

精华帖分享 | 因子构建思考1

本文来源于量化小论坛股票量化板块精华帖,作者为z-coffee。 以下为精华帖正文: 一段时间没写帖子,其实一直在研究策略,只是从不同的角度去思考而已。熟悉我的老板其实清楚,我的炉子水平一般,基本不太依托…

什么是 Web 应用中的 Facet 控件

在 Web 页面设计和开发中,facet 是一个十分重要的概念,尤其在电子商务、数据搜索和筛选功能中非常常见。Facet 通常指的是一种分类或过滤的方式,用于让用户能够通过多维度的条件来细化和调整数据结果,从而找到更符合需求的内容。F…

资源《Arduino 扩展板5-单电机驱动》说明。

资源链接: Arduino 扩展板5-单电机驱动 1.文件明细: 2.文件内容说明 包含:AD工程、原理图、PCB。 3.内容展示 4.简述 该文件为PCB工程,采用AD做的。 该文件打板后配合Arduino使用,属于Arduino的扩展板。

ECP 集成字段非必填配置

导读 INTRODUCTION 非必填设置:ECP主数据同步的时候,经常遇到一个问题,就是ECP报错,但是这个字段两边的ecp顾问与sf顾问都觉得没实际意思,觉得没有传输的必要性,这个时候我们就可以考虑非必输的字段不必输…

Linux命令--04----文件目录类命令

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 文件目录类命令pwdls 、llcdmkdirrmdirtouchcprmmv 查看文件catmorelessheadtail 打印信息echo\> 覆盖和>>追加lnhistory 文件目录类命令 pwd ls 、ll cd…

C--编译和链接见解

欢迎各位看官!如果您觉得这篇文章对您有帮助的话 欢迎您分享给更多人哦 感谢大家的点赞收藏评论 感谢各位看官的支持!!! 一:翻译环境和运行环境 在ANSIIC的任何一种实现中,存在两个不同的环境1,…

动手学深度学习(李沐)PyTorch 第 7 章 现代卷积神经网络

7.1 深度卷积神经网络(AlexNet) 在计算机视觉中,直接将神经网络与其他机器学习方法进行比较也许不公平。这是因为,卷积神经网络的输入是由原始像素值或是经过简单预处理(例如居中、缩放)的像素值组成的。但…

【C++】——list的介绍和模拟实现

P. S.:以下代码均在VS2019环境下测试,不代表所有编译器均可通过。 P. S.:测试代码均未展示头文件stdio.h的声明,使用时请自行添加。 博主主页:Yan. yan.                        …

亚马逊云乱扣费,被不知不觉扣钱真的好气呀

之前申请了亚马逊云12个月免费,一直也没用,也没登录,今天突然看到银行扣费信息,扣了我21美元,已经扣了我几个月了,登录亚马逊后台,实例为0的情况下,每个月都能扣21美元,国…

【HTML+CSS】留言板plus实现全过程

创建一个具有动态留言的简约风格留言板 在本教程中,我们将学习如何创建一个简约风格的留言板,它具备动态留言显示和一些基本动画效果。这个留言板将使用HTML和CSS构建,最终实现一个既美观又实用的界面。 准备工作 首先,确保你的…