(本次演示使用定制客户端演示,后面会专门讲客户端)
1、查询在线用户
在go语言中使用tcp_socket实现双向聊天室的功能,主要参考B站刘丹冰老师的视频,增加了许多日志输出,使逻辑更加清晰,通过这个项目可以把之前学的都串起来,是一个很简单的练手小项目。
值得一提的是,python我也写过类似的—>Python利用tcp_socket实现文件下载器
参考视频:8小时转职Golang工程师(刘丹冰)
本次代码也可以在我的码云上dev分支找到,传送门:https://gitee.com/noovertime/golang-test
启动Server: 在代码目录下执行 go run .
package main
func main() {
server := NewServer("", 7788)
server.Start()
}
package main
import (
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
)
type Server struct {
Ip string
Port int
//在线用户的列表
OnlineMap map[string]*User
mapLock sync.RWMutex
//消息广播的channel
Message chan string
}
var (
WarningLogger *log.Logger
InfoLogger *log.Logger
ErrorLogger *log.Logger
)
func init() {
mw := io.MultiWriter(os.Stdout)
InfoLogger = log.New(mw, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile)
WarningLogger = log.New(mw, "Warning: ", log.Ldate|log.Ltime|log.Lshortfile)
ErrorLogger = log.New(mw, "Error: ", log.Ldate|log.Ltime|log.Lshortfile)
}
func NewServer(ip string, port int) *Server {
//形参传递给结构体
server := &Server{
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}
//监听Message广播消息的go程,一旦有消息就发送给全部的在线的user
func (s *Server) ListenMessager() {
for {
msg := <-s.Message
InfoLogger.Println("ListenMessager获取Message中的消息:", msg)
//消息就发送给全部的在线的user
s.mapLock.Lock()
for user, cli := range s.OnlineMap {
InfoLogger.Println("发送消息到channel", user)
cli.UserChan <- msg
}
s.mapLock.Unlock()
}
}
//广播消息的方法
func (s *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
InfoLogger.Println("广播handler发送消息到Server的Message channel", sendMsg)
s.Message <- sendMsg
}
func (s *Server) Handler(conn net.Conn) {
InfoLogger.Println("连接建立成功", "协议:", conn.RemoteAddr().Network())
//实例化对象user
user := NerUser(conn, s)
user.Online()
isLive := make(chan bool) // 定义一个channel,用来判断是否活跃
//接受客户端传递的消息
go func() {
buf := make([]byte, 4096)
for {
n, err := conn.Read(buf)
if n == 0 {
user.Offline()
return
}
if err != nil && err != io.EOF {
fmt.Println("conn read err", err)
return
}
// 提取用户的消息,去除\n
msg := string(buf[:n-1])
// 将提取到的消息进行广播
user.DoMessage(msg)
isLive <- true //发送消息判定活跃,向islive发送true
}
}()
//当前handler阻塞
for {
select {
case <-isLive: //当前用户时活跃的,应该重置定时器
//不做任何事情,为了激活select,重置定制器
case <-time.After(time.Second * 60):
user.SendMsg_oneuser("您已超过六十秒不活跃,强制退出")
close(user.UserChan)
conn.Close()
return
}
}
}
//启动服务器的接口
func (s *Server) Start() {
InfoLogger.Printf("IP = %v,port = %d\n", s.Ip, s.Port)
//listen
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Ip, s.Port))
if err != nil {
fmt.Println("net.listen err", err)
panic(err)
}
//close socket
defer listener.Close()
//启动监听Message的gorouting
InfoLogger.Println("启动监听message协程")
go s.ListenMessager()
//accept
InfoLogger.Println("开始接收请求")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener accept err", err)
continue
}
//do handler
InfoLogger.Println("启动主handler")
go s.Handler(conn)
}
}
package main
import (
"net"
"strconv"
"strings"
)
type User struct {
Name string
Addr string
UserChan chan string
conn net.Conn
server *Server
}
func NerUser(conn net.Conn, server *Server) *User {
userAddr := conn.RemoteAddr().String()
user := &User{Name: userAddr, Addr: userAddr, UserChan: make(chan string), conn: conn, server: server}
InfoLogger.Println("启动UserListenMessage监听UserChan")
//启动监听当前用户go程
go user.UserListenMessage()
return user
}
//监听当前 User channel的方法,一旦有消息,就直接发送给对方客户端
func (u *User) UserListenMessage() {
for {
msg := <-u.UserChan
InfoLogger.Println("成功获取UserChan中的消息", msg)
_, err := u.conn.Write([]byte(msg + "\n"))
if err != nil {
ErrorLogger.Println("发送失败")
return
} else {
InfoLogger.Println(u.Name, "发送消息成功")
}
}
}
func (u *User) Online() {
u.server.mapLock.Lock()
//用户上线,将用户加入到onlinemap中
u.server.OnlineMap[u.Name] = u
InfoLogger.Println("用户上线,将用户加入到onlinemap中", u.server.OnlineMap)
u.server.BroadCast(u, "用户上线")
u.server.mapLock.Unlock()
}
func (u *User) Offline() {
u.server.mapLock.Lock()
//用户下线,将用户从当前map中删除
delete(u.server.OnlineMap, u.Name)
InfoLogger.Println("用户下线,移除:", u.Name)
u.server.BroadCast(u, "用户下线")
u.server.mapLock.Unlock()
}
func (u *User) SendMsg_oneuser(msg string) {
InfoLogger.Printf("发送msg: %v,给指定用户[%v]", msg, u.Name)
u.conn.Write([]byte(msg))
}
//实现who接口,遍历当前用户列表
func (u *User) Who() {
u.server.mapLock.Lock()
defer u.server.mapLock.Unlock()
for _, user := range u.server.OnlineMap {
onelinemsg := "当前在线人数为:" + strconv.Itoa(len(u.server.OnlineMap)) + "人" + "[" + user.Addr + "]" + user.Name + ": 在线\n"
u.SendMsg_oneuser(onelinemsg)
}
}
//用户处理消息的业务
func (u *User) DoMessage(msg string) {
InfoLogger.Println("用户输入:", msg)
if msg != "" {
if msg == "who" || msg == "Who" {
u.Who()
} else if len(msg) > 7 && msg[:7] == "rename|" { //判断为修改用户名功能
//消息格式: rename|张三
newName := strings.Split(msg, "|")[1] //通过字符串分割获取要修改的用户名
_, ok := u.server.OnlineMap[newName]
if ok {
u.SendMsg_oneuser("当前用户名已经被使用,修改失败")
} else {
u.server.mapLock.Lock()
defer u.server.mapLock.Unlock()
delete(u.server.OnlineMap, u.Name) //删除旧的key-value
InfoLogger.Println(u.Name, "修改为", newName)
u.server.OnlineMap[newName] = u //新增新的用户名:指针
u.Name = newName
u.SendMsg_oneuser(u.Name + "修改成功")
}
} else if len(msg) > 4 && msg[:3] == "to|" { //判断为私聊用户功能
remoteName := strings.Split(msg, "|")[1]
if remoteName == "" {
u.SendMsg_oneuser("您的输入有误,example: to|somebody|content ")
return
}
remoteUser, ok := u.server.OnlineMap[remoteName]
if !ok {
u.SendMsg_oneuser("用户不存在")
return
}
content := strings.Split(msg, "|")[2]
if content == "" {
u.SendMsg_oneuser("您的输入有误,example: to|somebody|content ")
return
}
remoteUser.SendMsg_oneuser(u.Name + ":" + content)
} else {
u.server.BroadCast(u, msg)
}
} else {
u.SendMsg_oneuser("您当前输入为空,请重新输入,Example: 更改用户名:rename|xxx;发起私聊:to|somebody|content ")
}
}
客户端非常的简单,随便写写
package main
import (
"flag"
"fmt"
"io"
"net"
"os"
)
type Client struct {
ServerIP string
ServerPort int
Name string
conn net.Conn
Flag int
}
var serverip string
var serverport int
func init() {
flag.StringVar(&serverip, "ip", "127.0.0.1", "设置server的地址 默认127.0.0.1")
flag.IntVar(&serverport, "port", 7788, "设置server的端口 默认7788")
}
func Newclient(serverip string, serverport int) *Client {
Client := &Client{
ServerIP: serverip,
ServerPort: serverport,
Flag: 999,
}
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", serverip, serverport))
if err != nil {
fmt.Println("connect fail,please check IP and Port is or not correct")
return nil
}
Client.conn = conn
return Client
}
func (c *Client) Menu() bool {
var flag int
fmt.Println("1.公聊模式")
fmt.Println("2.私聊模式")
fmt.Println("3.更新用户名")
fmt.Println("4.查询在线用户")
fmt.Println("0.退出")
fmt.Scanln(&flag)
if flag >= 0 && flag <= 4 {
c.Flag = flag
return true
} else {
fmt.Println("请输入合法的数字")
return false
}
}
func (c *Client) Run() {
for c.Flag != 0 {
for {
if c.Menu() {
break
}
}
//根据不同模式处理不同的业务
switch c.Flag {
case 1:
//公聊模式
c.PublicChat()
case 2:
c.PrivateChat()
case 3:
c.UpdateName()
case 4:
c.SelectUsers()
case 0:
fmt.Println("quit")
}
}
}
func (c *Client) UpdateName() bool {
fmt.Println("[更新用户名] 请输入用户名:")
fmt.Scanln(&c.Name)
sendMsg := "rename|" + c.Name + "\n"
_, err := c.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("[更新用户名] 发送失败")
return false
}
return true
}
func (c *Client) PublicChat() {
var chatmsg string
fmt.Println("[公聊模式] 请输入聊天内容: exit退出")
fmt.Scanln(&chatmsg)
for chatmsg != "exit" {
if len(chatmsg) > 0 {
sendmsg := chatmsg + "\n"
_, err := c.conn.Write([]byte(sendmsg))
if err != nil {
fmt.Println("[公聊模式]发送失败")
break
}
}
chatmsg = ""
fmt.Println("[公聊模式] 请输入聊天内容: exit退出")
fmt.Scanln(&chatmsg)
}
}
func (c *Client) SelectUsers() {
sendMsg := "who" + "\n"
_, err := c.conn.Write([]byte(sendMsg))
if err != nil {
fmt.Println("发送失败")
}
}
func (c *Client) PrivateChat() {
c.SelectUsers()
var remote string
var msg string
fmt.Println("[私聊模式] 请输入私聊对象: exit退出")
fmt.Scanln(&remote)
for remote != "exit" {
fmt.Println("[私聊模式] 请输入聊天内容: exit退出")
fmt.Scanln(&msg)
for msg != "exit" {
if len(msg) > 0 {
sendmsg := "to|" + remote + "|" + msg + "\n\n"
_, err := c.conn.Write([]byte(sendmsg))
if err != nil {
fmt.Println("[私聊模式] 发送失败")
break
}
}
msg = ""
fmt.Println("[私聊模式] 请输入聊天内容: exit退出")
fmt.Scanln(&msg)
}
c.SelectUsers()
fmt.Println("[私聊模式] 请输入私聊对象: exit退出")
fmt.Scanln(&remote)
}
}
//处理响应
func (c *Client) ReadResponse() {
io.Copy(os.Stdout, c.conn)
}
func main() {
flag.Parse()
client := Newclient(serverip, serverport)
if client == nil {
fmt.Println("连接服务器失败")
return
}
fmt.Println("连接服务器成功")
go client.ReadResponse()
client.Run()
}
注:python版本目前只实现了who接口,也就是查询当前在线用户,其他接口不想写了暂时没有实现,不过实现逻辑都是一样的,就是根据socket接收到的数据进行判断
用户上线发送消息
后台日志打印:
用户广播消息:
用户查询在线列表:
关于代码逻辑与上面的golang一毛一样,所以在这里就不在赘述了,看上面的就行,架构图太简单了,不想画,就这样吧,那么上代码!
注意: 当根据收到data进行判断时,python转换为str类型前面会多一个空格,所以我们在做判断时,一定要注意字符串的切割,如: data[1:3] == 'ho’
import socket
import threading
import logging
## 日志模块
Format = logging.Formatter('%(levelname)s %(asctime)s %(filename)s %(funcName)s [%(message)s] ')
logger = logging.getLogger()
logger.setLevel('DEBUG')
console_handle = logging.StreamHandler()
console_handle.setLevel(level='INFO')
console_handle.setFormatter(Format)
logger.addHandler(console_handle)
class Server(object):
## 创建socket,绑定端口
def __init__(self) -> None:
self.onlinePool = {}
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
self.socket.bind(('', 7788))
## start方法启动服务
def start(self):
self.socket.listen(128)
while True:
client_socket, info = self.socket.accept()
logger.info("接收请求")
t1 = threading.Thread(target=self.Domessage,
args=(client_socket, info))
t1.setDaemon(True)
t1.start()
## Domessage处理收到的消息,并根据消息做出相应判断,相当于一个router
def Domessage(self, client_socket, info):
self.login(client_socket, info)
while True:
try:
data = client_socket.recv(1024).decode()
## 当根据收到data进行判断时,python转换为str类型前面会多一个空格,所以我们在做判断时,一定要注意字符串的切割
logger.debug("data=",data[1:3],len(data))
if data[1:] != "":
if len(data) == 0:
logger.info(f"{info}断开")
self.logout(client_socket, info)
break
elif data[1:3] == 'ho' :
self.show(info)
else:
self.broadcast(data)
else:
self.Send_one_msg(info,"您输入的为空请重新输入")
except Exception as e:
self.logout(client_socket=client_socket, info=info)
break
## 用户登录的时候会调用它,作用:广播用户登录消息
def login(self, client_socket, info):
self.onlinePool[info] = client_socket
logger.info(f"{info} login")
self.broadcast("上线")
## 用户下线的时候会调用它,作用:广播用户下线消息,关闭client_socket
def logout(self, client_socket, info):
del self.onlinePool[info]
logger.info(f"{info} logout")
client_socket.close()
self.broadcast("下线")
## 广播方法,遍历onlinePool,向所有的client_socket发送消息
def broadcast(self, msg):
for i in self.onlinePool:
data = str(i) + ":"+msg+"\n"
self.onlinePool[i].send(data.encode("utf-8"))
## 向单独用户发送消息
def Send_one_msg(self, info, msg):
self.onlinePool[info].send(msg.encode("utf-8"))
## 展示当前在线用户
def show(self, info):
msg = '当前在线用户:\n'
for i in self.onlinePool:
msg += (str(i) + '\n')
self.Send_one_msg(info, msg=msg)
def main():
server = Server()
server.start()
if __name__ == "__main__":
main()
复用golang客户端,效果一样一样的