实战教程适用于有一定golang的基础,或者是认真读完前几篇的小伙伴,实战仅供语法巩固,并非企业版
往期回顾
基础server构建
写两个文件,一个文件(server.go)里写核心服务器的程序,另一个文件(main.go)里写启动服务器的程序
server.go
package main
import (
"fmt"
"net"
)
type Server struct {
Ip string
Port int
}
//创建一个server对象
func Newserver(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
}
return server
}
func (this *Server) Handler(conn net.Conn) {
//连接
fmt.Println("连接成功")
}
//启动server服务
func (this *Server) Start() {
//listen
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
for {
//accept
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err:", err)
continue
}
//do handler
go this.Handler(conn)
}
//close
defer listener.Close()
}
功能很简单,就是一直循环监听一个端口,如果有连接了就打印连接成功。先创建一个server结构体,然后再创建一个server对象,给创建两个类方法Start()和Handler()分别用于启动和处理连接后的逻辑,
这里用大写表示其他文件也可以访问到。handler那里启动一个go程用来异步处理连接。关闭的话只需用derfer处理在函数生命周期结束之前关闭就行
main.go
package main
func main() {
server := Newserver("127.0.0.1", 8888)
server.Start()
}
两个文件同时编译运行在浏览器输入:127.0.0.1:8888就可以在控制台成功看到:连接成功的输出
用户上线及广播的功能
要实现的功能是:上线一个新用户,那个新用户会收到已上线的提示,同时,其他在线用户也会收到已上线的提示。
这时候就需要存储所用的上线用户了,复杂了用数据库,这里我们只为了简单实现只需要创建一个用户的hash map即可。上一个功能里处理用户上线的handler函数只在用户上线的时候打印了上线成功,我们只需要新增一点处理逻辑就行。
在上线的同时,新建一个用户对象,每个用户都有他自己对应的channel,以及自己的地址名字,用户上线时将用户的地址名字作为键,用户对象作为值传入那个用户的hash map中。在用户新建的时候开启一个新的函数一直监听自己的channel中是否消息,如果有的话反馈给客户端即可,
最后只需要在server中创建一个广播函数,一但有用户上线就“将xxx上线成功”这句话写入channel中。
为了使代码可读性更高,将用户行为些逻辑单独抽离出来写道一个新的文件中
user.go
package main
import "net"
type User struct {
Name string
Addr string
C chan string
conn net.Conn
}
func (this *User) ListenMessage() {
for {
msg := <-this.C
this.conn.Write([]byte(msg + "\n"))
}
}
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String() //这句话可以获取客户端地址
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn, //新建的时候需要与服务器产生练习,于是传入这个conn对象
}
go user.ListenMessage() //这个函数一直监听着自己的channel管道,只要有消息,就写入conn
return user
}
这时候server.go也需要稍作修改,增加发消息的函数,结构体新增map
package main
import (
"fmt"
"net"
"sync"
)
type Server struct {
Ip string
Port int
//存储用户的map
OnlineMap map[string]*User
mapLock sync.RWMutex //由于这个OnlineMap 是全局的变量,所以为了保证数据的准确性需要上锁
}
//创建一个server对象
func Newserver(ip string, port int) *Server {
server := &Server{
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
}
return server
}
func (this *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
this.mapLock.Lock()
for _, client := range this.OnlineMap { //一但有用户上线循环用户map将消息告诉每一个用户
client.C <- sendMsg
}
this.mapLock.Unlock()
}
func (this *Server) Handler(conn net.Conn) {
//连接
user := NewUser(conn)
this.mapLock.Lock()
this.OnlineMap[user.Name] = user
this.mapLock.Unlock()
this.BroadCast(user, "connect success")
}
//启动server服务
func (this *Server) Start() {
//listen
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", this.Ip, this.Port))
if err != nil {
fmt.Println("net.Listen err:", err)
return
}
for {
//accept
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err:", err)
continue
}
//do handler
go this.Handler(conn)
}
//close
defer listener.Close()
}

使用nc模拟两个客户端连接的过程
成功实现我们想要的功能,只需nc是什么以及怎么用自行百度,这里不再赘述
用户消息广播
上一个例子中,我们对用户上线进行了广播,那么接下来就好写了,因为这个广播函数是可以复用的,只需要将connect success换成我们从客户端读取过来的消息即可
从客户端读消息用Read()方法,它接受的参数是读取的长度。在客户端上线广播发送后创建一个新的go程用来循环读取客户端消息并且发送,当然直接写代码也无所谓。主要是体现一下有专门的go程用来处理消息广播更加专业一点。
异常处理:当读的读写为0的时候就代表客户端合法关闭了,那么这时候应该输出下线,当有错误的时候且错误不是io.EOF(读到文件的末尾)时,也要打印出错误信息
只需在handler函数中新增代码(旧代码用…代替):
func (this *Server) Handler(conn net.Conn) {
...
this.BroadCast(user, "connect success")
...
buf := make([]byte, 4096)
go func() {
for {
n, err := conn.Read(buf)
if n == 0 {
fmt.Println("offline")
this.BroadCast(user,"offline")
return
}
if err != nil && err != io.EOF {
fmt.Println(err)
return
}
msg := string(buf[:n-1]) //把末尾的回车去掉
this.BroadCast(user, msg)
}
}()
}
优雅的代码
有没有发现我们的代码目前很不优雅,用户的下线上线,消息等逻辑都在server.go里面,我们这次需要把这些抽离到user.go中。
比如发送消息 this.BroadCast(user, msg),当把这句话抽离的user.go里面肯定不能用this访问这个BroadCast方法了,因为这是server的方法,所以在创建user对象时我们需要新传入一个server对象,以便可以调用它里面的方法。user.go新增函数:Online(),Offline(),SendMsg()
user.go
package main
import "net"
type User struct {
Name string
Addr string
C chan string
conn net.Conn
server *Server
}
func (this *User) ListenMessage() {
for {
msg := <-this.C
this.conn.Write([]byte(msg + "\n"))
}
}
func (this *User) Online() {
this.server.mapLock.Lock()
this.server.OnlineMap[this.Name] = this
this.server.mapLock.Unlock()
this.server.BroadCast(this, "connect success")
}
func (this *User) Offline() {
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.mapLock.Unlock()
this.server.BroadCast(this, "offline")
}
func (this *User) SendMsg(msg string) {
this.server.BroadCast(this, msg)
}
func NewUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
server: server,
}
go user.ListenMessage()
return user
}
server.go中的handler变成了
func (this *Server) Handler(conn net.Conn) {
//连接
user := NewUser(conn, this)
user.Online()
buf := make([]byte, 4096)
go func() {
for {
n, err := conn.Read(buf)
if n == 0 {
user.Offline()
return
}
if err != nil && err != io.EOF {
fmt.Println(err)
return
}
msg := string(buf[:n-1]) //把末尾的回车去掉
user.SendMsg(msg)
}
}()
}
可读性是不是高多了
在线用户查询
查询用户就不是广播了,是单播,只需要给发送查询指令的那个用户发送结果。至于怎么处理查询指令,我们只需要在用户的SendMsg里加一个判断,在发送消息前判断知否为查询在线用户指令,不是的话去发送广播,是的话单播
//发送消息给指定用户
func (this *User) SenToUser(msg string) {
this.conn.Write([]byte(msg))
}
func (this *User) SendMsg(msg string) {
if msg == "users" {
this.server.mapLock.Lock()
for _, user := range this.server.OnlineMap {
OnlineMsg := "User " + user.Name + " Is Online\n"
this.SenToUser(OnlineMsg)
}
this.server.mapLock.Unlock()
} else {
this.server.BroadCast(this, msg)
}
}
修改用户名
和上个例子一样,只需要修改user的SendMsg函数就行,多加一个判断,判断用户发来的消息是否为修改名字的指令
判断要修改的名字是否已经存在,存在的话修改失败,不存在的话修改成功
func (this *User) SendMsg(msg string) {
'''
'''
else if len(msg) > 7 && msg[:7] == "rename|" {
newName := strings.Split(msg, "|")[1]
_, ok := this.server.OnlineMap[newName]
if ok {
this.SenToUser("sorry, this name is already used\n")
} else {
this.server.mapLock.Lock()
delete(this.server.OnlineMap, this.Name)
this.server.OnlineMap[newName] = this
this.server.mapLock.Unlock()
this.Name = newName
this.SenToUser("success\n")
}
'''
'''
}

超时强踢功能
当客户端一段时间没发送消息,我们就认为它不再活跃,把它踢出,要想实现这个功能,用到的东西就是计时器。设定一计时器比如10秒一但它计时完成那么就执行踢出用户的功能,还需要一个更新计时器判断,用户有消息输入时就重置计时器,并且认为改用户是活跃的。
思考一下之前学过的啥知识能满足这个问题。没错!就是selcet与case。
time.After(time.Second*10)这是计时器的写法,计时10秒,把它当作条件传入case,计时完成,这个case下的代码就会被执行。
那么如何重置这个计时器呢?答案就是再执行一遍这个代码,它自然就被重置了。
func (this *Server) Handler(conn net.Conn) {
...
...
//定义是否活跃
isLive := make(chan bool)
go func() {
for {
...
...
isLive <- true
}
}()
for {sil
select {
case <-isLive:
case <-time.After(time.Second * 10):
//踢出超时用户
user.SenToUser("you have been kicked")
//主动销毁资源
close(user.C)
conn.Close()
return
}
}
}
当用户有输入的时候执行 isLive <- true,这时候 case <-isLive:条件触发这里面可以啥也不用写。因为我们只想让time.After(time.Second * 10)重新执行一遍重置定时器对吧,当isLive为True的时候就已经激活了select,select有个特点就是当所有case条件为true的时候,它会随机选择一个执行。那么它怎么知道其他case是否满足的呢?当然是执行一遍”<- “右边的代码,看是否满足条件。
私聊功能
很简单,类似于重置用户名和查询所有用户的功能,我甚至不准备写了,但是想了想还是写上了,因为毕竟这也是一个全新的功能。
只需要获取对方的名字,根据对方的名字在onlinemap中获取该对象,再调用对方的发送给指定用户的方法就行,类似于下线这种只告诉自己。
只不过这个消息变成了从别的客户端获取的了
func (this *User) SendMsg(msg string) {
'''
'''
else if len(msg) > 4 && msg[:3] == "to|" {
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
this.SenToUser("格式错误")
return
}
remoteUser, ok := this.server.OnlineMap[remoteName]
if !ok {
this.SenToUser("用户名不存在")
return
}
content := strings.Split(msg, "|")[2]
if content != "" {
remoteUser.SenToUser(this.Name + ":" + content)
}
}
'''
'''
}
客户端
之前发送请求使用nc模拟的,现在我们自己写一个客户端吧。写法和服务端差不多,只要实现接受用户输入的功能,过程不再赘述。客户端为了精简没有异常处理,如果需要自行加入
package main
import (
"flag"
"fmt"
"io"
"net"
"os"
)
type Client struct {
ServerIp string
ServerPort int
Name string
conn net.Conn
flag int
}
func NewCilent(serverIp string, serverPort int) *Client {
client := &Client{
ServerIp: serverIp,
ServerPort: serverPort,
flag: 666,
}
conn, _ := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
client.conn = conn
return client
}
var serverIp string
var serverPort int
func init() {
flag.StringVar(&serverIp, "ip", "127.0.0.1", "设置ip默认127.0.0.1")
flag.IntVar(&serverPort, "port", 8888, "设置端口默认8888")
}
func (client *Client) menu() bool {
var flag int
fmt.Println("1.talk with everybody")
fmt.Println("2.talk with the person")
fmt.Println("3.update username")
fmt.Println("4.exit")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 3 {
client.flag = flag
return true
} else {
return false
}
}
func (client *Client) UpdateName() bool {
fmt.Println("enter your username:")
fmt.Scanln(&client.Name)
sendMsg := "rename|" + client.Name + "\n"
client.conn.Write([]byte(sendMsg))
return true
}
func (client *Client) PublicChat() {
var Msg string
for Msg != "q" {
fmt.Println("please enter,enter q to exit")
fmt.Scanln(&Msg)
if len(Msg) != 0 {
sendMsg := Msg + "\n"
client.conn.Write([]byte(sendMsg))
}
Msg = ""
}
}
func (client *Client) GetUsers() {
sendMsg := "users\n"
client.conn.Write([]byte(sendMsg))
}
func (client *Client) PrivateChat() {
var remoteName string
var Msg string
client.GetUsers()
for Msg != "q" {
fmt.Println("请输入对方用户名|,q退出")
fmt.Scanln(&remoteName)
fmt.Println("请输入内容")
fmt.Scanln(&Msg)
if len(Msg) != 0 {
sendMsg := "to|" + remoteName + "|" + Msg + "\n"
client.conn.Write([]byte(sendMsg))
}
Msg = ""
client.GetUsers()
}
}
func (client *Client) DealResponse() {
io.Copy(os.Stdout, client.conn)
}
func (client *Client) Run() {
for client.flag != 0 {
for client.menu() != true {
}
switch client.flag {
case 1:
fmt.Println("talk with everybody")
client.PublicChat()
break
case 2:
fmt.Println("talk with the person")
client.PrivateChat()
break
case 3:
fmt.Println("update username")
client.UpdateName()
break
}
}
}
func main() {
flag.Parse()
client := NewCilent(serverIp, serverPort)
if client == nil {
return
}
go client.DealResponse()
fmt.Println("connect success")
client.Run()
}
单词数:866字符数:10286