文章目录
一、Golang版本
1.1 功能展示
(本次演示使用定制客户端演示,后面会专门讲客户端)
1、查询在线用户
2、用户上线,下线动态广播,超过60秒不活跃强制退出
3、支持修改用户名
4、两种消息模式,公聊与私聊
1.2 服务端
1.2.1 简介
在go语言中使用tcp_socket实现双向聊天室的功能,主要参考B站刘丹冰老师的视频,增加了许多日志输出,使逻辑更加清晰,通过这个项目可以把之前学的都串起来,是一个很简单的练手小项目。
值得一提的是,python我也写过类似的—>Python利用tcp_socket实现文件下载器
参考视频:8小时转职Golang工程师(刘丹冰)
1.2.2 架构图
现在看起来可能有点抽象,等熟悉代码逻辑后就理解了
1.2.3 代码逻辑
- 在main.go中使用NewServer函数实例化对象,使用start方法启动server端
- Start方法处理请求,并启动一个监听Message管道的gorouting:ListenMessager,最后调用Handler方法处理请求
- Handler方法判断用户是否在线,在线调用user.go中的Online方法,离线调用Offline方法,处理消息调用DoMessage方法,并且会像isLive管道发送消息,以判断是否活跃,超过60秒不活跃会close掉conn
- Online方法收到请求后,会把用户存到OnlineMap中,并调用BroadCast向channel中发送消息
- Offline方法收到请求后,会把用户从OnlineMap中删除,并调用BroadCast向channel中发送消息
- DoMessage方法收到请求后,会根据收到的消息进行相应的动作,如:消息体为who,则调用Who方法查看当前在线列表,因其他逻辑类似,不做赘述…
- BroadCast收到请求后,会像Message管道中发送消息,此时持续监听Message的ListenMessager解阻塞
- 在创建用户对象时,会为每个用户分配一个channel:UserChan,并启动一个持续监听用户channel消息的gorouting:UserListenMessage
- ListenMessager会遍历OnlineMap,OnlineMap中存放着每个用户channel的内存地址,向每个用户的channel:UserChan发送消息
- 此时每个用户的 UserListenMessage解阻塞,使用conn.Write方法向客户端发送消息
- 客户端收到消息
1.2.4 代码展示
本次代码也可以在我的码云上dev分支找到,传送门:https://gitee.com/noovertime/golang-test
启动Server: 在代码目录下执行 go run .
main.go
package main
func main() {
server := NewServer("", 7788)
server.Start()
}
server.go
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)
}
}
user.go
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 ")
}
}
1.3 客户端
客户端非常的简单,随便写写
1.3.1 代码逻辑
- 使用flag库获取参数
- 连接服务器
- Menu方法打印菜单并判断输入是否合法
- Menu方法判断成功且消息不为exit后,调用Run方法执行业务
- 一个swich调用各种函数,不做赘述了…
1.3.2 代码展示
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版本
注:python版本目前只实现了who接口,也就是查询当前在线用户,其他接口不想写了暂时没有实现,不过实现逻辑都是一样的,就是根据socket接收到的数据进行判断
2.1 功能展示
用户上线发送消息
后台日志打印:
用户广播消息:
用户查询在线列表:
2.1 服务端
关于代码逻辑与上面的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()
2.2 客户端
复用golang客户端,效果一样一样的
三、将来需要优化的方向
- 优化服务端,总觉得使用message来判断有点low,后面会改成接口类型
- 感兴趣的小伙伴可以吧python版本未实现的功能实现一下
- 单独写一个python的客户端
- 其他暂时没想到