面向对象编程使用Go与Python分别实现tcp多人聊天室

42 篇文章 1 订阅
15 篇文章 4 订阅

一、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 代码逻辑

服务端共分为3个文件

  1. 在main.go中使用NewServer函数实例化对象,使用start方法启动server端
  2. Start方法处理请求,并启动一个监听Message管道的gorouting:ListenMessager,最后调用Handler方法处理请求
  3. Handler方法判断用户是否在线,在线调用user.go中的Online方法,离线调用Offline方法,处理消息调用DoMessage方法,并且会像isLive管道发送消息,以判断是否活跃,超过60秒不活跃会close掉conn
  4. Online方法收到请求后,会把用户存到OnlineMap中,并调用BroadCast向channel中发送消息
  5. Offline方法收到请求后,会把用户从OnlineMap中删除,并调用BroadCast向channel中发送消息
  6. DoMessage方法收到请求后,会根据收到的消息进行相应的动作,如:消息体为who,则调用Who方法查看当前在线列表,因其他逻辑类似,不做赘述…
  7. BroadCast收到请求后,会像Message管道中发送消息,此时持续监听Message的ListenMessager解阻塞
  8. 在创建用户对象时,会为每个用户分配一个channel:UserChan,并启动一个持续监听用户channel消息的gorouting:UserListenMessage
  9. ListenMessager会遍历OnlineMap,OnlineMap中存放着每个用户channel的内存地址,向每个用户的channel:UserChan发送消息
  10. 此时每个用户的 UserListenMessage解阻塞,使用conn.Write方法向客户端发送消息
  11. 客户端收到消息

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 代码逻辑

  1. 使用flag库获取参数
  2. 连接服务器
  3. Menu方法打印菜单并判断输入是否合法
  4. Menu方法判断成功且消息不为exit后,调用Run方法执行业务
  5. 一个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客户端,效果一样一样的

三、将来需要优化的方向

  1. 优化服务端,总觉得使用message来判断有点low,后面会改成接口类型
  2. 感兴趣的小伙伴可以吧python版本未实现的功能实现一下
  3. 单独写一个python的客户端
  4. 其他暂时没想到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

陈小c

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值