练习
ps:目前只更新到 二 ,未完待续呀~
使用Go Module初始化项目(练习)
Ⅰ. 开启go module
go env -w GO111MODULE=on
※ Go Module的开关,推荐go v1.11以上版本都设置为on,它将指向未来版本。
Ⅱ. 初始化项目
为了与GOPATH区分,这里不在GOPATH下创建文件夹(即,不要求在GOPATH下创建文件夹)
mkdir -p wening/modules_test
👆创建属于你自己的文件夹
👇进入你的文件夹下,初始化Go Module模块,即创建go.mod
文件:
※ 建议跟上当前模块的名称,它决定我们未来导包时的路径名称。
> cd wening/modules_test
> go mod init github.com/wening/modules_test
go: creating new go.mod: module github.com/wening/modules_test
👇可以看一下这个go.mod
文件:
module github.com/wening/modules_test
go 1.17
有了这个文件就可以说明Go Module模块触发成功
Ⅲ. 编辑简易代码查看效果
创建main.go
,代码仅作示例参考不追究具体(作者给出的):
package main
import (
"fmt"
"github.com/aceld/zinx/ziface"
"github.com/aceld/zinx/znet"
)
//ping test 自定义路由
type PingRouter struct {
znet.BaseRouter
}
//Ping Handle
func (this *PingRouter) Handle(request ziface.IRequest) {
//先读取客户端的数据
fmt.Println("recv from client : msgId=", request.GetMsgID(), ", data=", string(request.GetData()))
//再回写ping...ping...ping
err := request.GetConnection().SendBuffMsg(0, []byte("ping...ping...ping"))
if err != nil {
fmt.Println(err)
}
}
func main() {
//1 创建一个server句柄
s := znet.NewServer()
//2 配置路由
s.AddRouter(0, &PingRouter{})
//3 开启服务
s.Serve()
}
- 普通导包(使用GOPATH)
进入到该目录,执行go get .../...
- Go Module导包
直接go get github.com/aceld/zinx/ziface
,go get github.com/aceld/zinx/znet
,从网络自动下载包文件。
此时得到go.sum
文件,且go.mod
文件中多了一个需要的库。
现在直接运行go run main.go
运行成功~
来看下go.mod
文件中多出来的内容:
require github.com/aceld/zinx v1.0.0 // indirect
github.com/aceld/zinx
包,v1.0.0
版本号, // indirect
表示间接依赖的库
go.sum
罗列当前项目直接/间接依赖的所有版本模块,防篡改,保证完整性。只有两种方式:① h1+hash
保证当前包的完整性(所有项目文件的校验和生成的hash,不存在可能表示依赖库暂时未启用);② xxx/go.mod h1+hash
只能保证go.mod
文件中的完整性
另外,刚刚下载的包存到哪里了呢?这里:$GOPATH\pkg\mod\github.com\aceld
这样做的好处就是,方便库包的管理,一般都会下载到$GOPATH\pkg\mod\
这里,直接下载,不需要再特地进入某文件夹下去下载。
Ⅳ. 修改项目模块版本的依赖关系
可能的需求场景:版本更新覆盖,想启用老版本;也可用于国外版本镜像转换~
go mod edit -replace=新版本名=老版本名
此时go.mod
将多一行replace...
项目案例——即时通信系统
一 构建基础Server
首先创建你的项目文件夹,以及其下的两个文件:server.go
和main.go
(经验之谈:这样做的目的是为了模块基本划分,server.go
用于完成服务端的基本构件,main.go
用于程序主入口)
/*
* server.go
* server基本的listen操作
*/
package main
import (
"fmt"
"net"
)
//结构体:ip和端口
type Serer struct {
Ip string
Port int
}
//基本封装:创建一个server的接口
func NewServer(ip string, port int) *Serer {
//创建基本的server对象(相当于将Server地址赋给server,指针接收)
server := &Serer{
Ip: ip,
Port: port,
}
return server
}
func (this *Serer) Handler(conn net.Conn) { //为创建好的连接启动一个Handler
//...当前链接业务
fmt.Println("链接建立成功!")
}
//启动服务器的接口(首字母大写表示对外开放),启动上面接口的具体方法,分四步
func (this *Serer) Start() {
//socket listen,传网络类型和服务器地址(一般ip=127.0.0.1:8888,我们需要拼接ip和port,格式化一下)
listener,err := net.Listen("tcp",fmt.Sprintf("%s:%d",this.Ip,this.Port))
if err != nil { //捕获err基本初类型
fmt.Println("net.Listen err:",err)
return
}
//close listen socket
defer listener.Close() //别忘了释放
for {
//accept 成功返回连接(之后就可进行基本读写操作) 阻塞循环
conn, err := listener.Accept()
if err != nil { //失败情况
fmt.Println("listener accept err:",err)
continue
}
//do handler 业务
go this.Handler(conn) //当前go层承载业务,异步协程
}
}
🧐 go并发 了解一下~
/*
* main.go
* server入口
*/
package main //都属于main包,就不用import了
func main() {
server := NewServer("127.0.0.1", 8888) //监听
server.Start() //启动
}
编译:(注意,linux下不用写后缀,windows下要建立.exe文件,不然windows找不到打开方式)
> go build -o server.exe main.go server.go
> ls
目录: D:\Go\PATH\src\GolangStudy\14-golang-IM-System
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 2021/11/4 12:57 174 main.go
-a---- 2021/11/4 12:58 2397184 server.exe
-a---- 2021/11/4 12:55 1412 server.go
启动(为方便叙述,称此客户端为s1):
> ./server.exe
另开一个客户端(称此客户端为s2),使用指令模拟基本tcp客户端(演示为windows下的):
> telnet 127.0.0.1 8888
正在连接127.0.0.1...
(Linux 使用原生指令 nc ,windows 使用 telnet)
windows 开启 telnet 方法:
此时刚刚的客户端(即s1)显示如下:
> ./server.exe
链接建立成功!
二 用户上线功能
首先需要增加一个用户类
/*
* user.go
*/
package main
import "net"
//用户名、地址、与当前用户绑定的channel(每个用户都有一个)、当前用户唯一可以和客户端通信的链接
type User struct {
Name string
Addr string
C chan string
conn net.Conn
}
//创建一个用户的API
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String() //从链接获取地址
user := &User{
Name: userAddr, //可以以当前地址作为名称
Addr: userAddr,
C: make(chan string), //看来可以创建一个channel,make分配空间
conn: conn,
}
//启动监听当前user channel消息的goroutine
go user.ListenMessage()
return user
}
//每个user都该启动一个go(goroutine),将启动信息发给客户端。
//监听当前User channel的方法,一旦有消息就直接发给客户端
func (user *User) ListenMessage() {
for {
msg := <-user.C //永远读取,写到channel中去
user.conn.Write([]byte(msg + "\n")) //转二进制传输
}
}
要实现上线提示功能,需要server得知上线,添加进用户表,并广播。server.go的具体改动如下:
//结构体中增加用户表和消息管道
type Server struct {
Ip string
Port int
//增加map表和管道
//在线用户列表
OnLineMap map[string]*User
mapLock sync.RWMutex //加一个执行锁
//消息广播channel
Message chan string
}
//结构体变更,server接口也要跟着动
func NewServer(ip string, port int) *Server {
//创建基本的server对象(相当于将Server地址赋给server,指针接收)
server := &Server{
Ip: ip,
Port: port,
OnLineMap: make(map[string]*User), //在线用户列表
Message: make(chan string), //消息广播channel
}
return server
}
//既然有了具体业务了,我们把事宜就写到Handler里去
func (this *Server) Handler(conn net.Conn) { //为创建好的连接启动一个Handler
//...当前链接业务
fmt.Println("链接建立成功!")
user := NewUser(conn)
//用户上线,更新在线用户列表
this.mapLock.Lock() //上锁
this.OnLineMap[user.Name] = user
this.mapLock.Unlock() //解锁
//广播当前用户上线消息
this.BroadCast(user, "已上线!")
//当前handler阻塞(handler暂时不能死,因为那将导致上述go程死亡,其子go程都将死亡)
select {}
}
//这个业务用到的广播方法
//广播消息的方法,需要知道是谁发起的,以及内容是什么
func (this *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
this.Message <- sendMsg
}
/* 向Start()的循环前增加消息监听!!!!!!
//启动加载 Message 的 goroutine
go this.ListenMessage()
*/
//listen启动时即启动的永久go程,监听Message广播消息channel的goroutine,一旦有消息就发送给全部的在线User
func (this *Server) ListenMessage() {
for {
msg := <-this.Message
//将mag发送给全部在线的user
this.mapLock.Lock() //加锁
for _, cli := range this.OnLineMap {
cli.C <- msg //像每个用户分别发送消息
}
this.mapLock.Unlock() //解锁
}
}
注:
<-
是对chan
类型来说的。
chan
类型类似于一个数组, 当<- chan
的时候是对chan中的数据读取; 相反chan <- value
是对chan
赋值。
效果如图:
针对汉字乱码问题:windows 控制台cmd乱码的解决办法