刘丹冰即时通信系统实现与思考

前言

参考链接:

37-项目案例-即时通信系统-课程介绍_哔哩哔哩_bilibili

六、案例-即时通信系统

《8小时转职Golang工程师》超时强踢功能的BUG修复 - Blog of Code

收获

代码:go_study/base/IM-System at main · BlueCestbon/go_study · GitHub

语雀笔记:https://www.yuque.com/xw76/back/ftwkp7y1c1x77rf0

Go语言学习中,第一个系统级的 Demo,如同刘丹冰老师所说,串联起来了基础的 Go 知识。

  1. 如何搭建这个通信系统?得有支持连接的 server 吧,用户上线得有注册和提醒吧,用户能发送消息(私聊、公聊),用户能改自己用户名,用户超时强踢。
  2. net包下的属性/方法,net.Listen(), net.Conn, net.Conn.Write()/Read()/Close(), net.Dial()
  3. 强化了基础语法,对象的接口实现、channel 的用法
  4. 超时强踢导致 CPU 占用率高,读取了关闭的 channel 得到对应的零值

1. 基础server搭建

一个 server 能实现启动一个进程,监听一个 port ,接收到 tcp 请求之后能响应

1.1. 代码

package main

import (
	"fmt"
	"net"
)

type Server struct {
	Ip   string
	Port int
}

func NewServer(ip string, port int) *Server {
	return &Server{
		Ip:   ip,
		Port: port,
	}
}

func (server *Server) Handler(conn net.Conn) {
	fmt.Println("成功建立连接")
}

// Start 启动服务器的接口
func (server *Server) Start() {
	// socket listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", server.Ip, server.Port))
	if err != nil {
		fmt.Println("net.Listen err: ", err)
		return
	}

	// close
	defer listener.Close()

	for {
		// accept
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener conn err: ", err)
			continue
		}
		// handle
		go server.Handler(conn)
	}
}
package main

func main() {
	// 因为我需要在wsl里访问win的机器,写的地址就不能是127.0.0.1
	server := NewServer("0.0.0.0", 8888)
	server.Start()
}

1.2. 结果展示

2. 用户上线及广播功能

客户端(得有 user 类)上线之后,先去 server 记录一下(onlineMap 和 channel),server 一直监听自己的 channel,发现自己的 channel 有东西了,就遍历当前的 onlineMap,给其他 user 发消息(user 也要有 channel),user 监听自己的 channel,发现有东西了,就写到客户端那。

2.1. 代码

package main

import (
	"fmt"
	"net"
	"sync"
)

type Server struct {
	Ip   string
	Port int
	// 在线的用户
	OnlineMap map[string]*User
	mapLock   sync.RWMutex
	// server监控channel,有信息就触发广播
	ServerChannel chan string
}

func NewServer(ip string, port int) *Server {
	return &Server{
		Ip:            ip,
		Port:          port,
		OnlineMap:     make(map[string]*User),
		ServerChannel: make(chan string),
	}
}

// BroadCast 广播消息
func (server *Server) BroadCast(user *User, msg string) {
	sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg
	server.ServerChannel <- sendMsg
}

// Handler 处理连接上线之后的操作
func (server *Server) Handler(conn net.Conn) {
	// fmt.Println("成功建立连接")
	// 用户上线,添加到OnlineMap
	user := NewUser(conn)
	server.mapLock.Lock()
	server.OnlineMap[user.Name] = user
	server.mapLock.Unlock()

	// 广播当前用户上线消息
	go server.BroadCast(user, "已上线")

	// 阻塞当前的handler
	select {}
}

// ListenServerChannel 监听server的广播信道,一旦有消息,就发送给全部在线的user
func (server *Server) ListenServerChannel() {
	for {
		sendMsg := <-server.ServerChannel
		server.mapLock.Lock()
		for _, user := range server.OnlineMap {
			user.C <- sendMsg
		}
		server.mapLock.Unlock()
	}
}

// Start 启动服务器的接口
func (server *Server) Start() {
	// socket listen
	listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", server.Ip, server.Port))
	if err != nil {
		fmt.Println("net.Listen err: ", err)
		return
	}

	// 监听sever的广播信道
	go server.ListenServerChannel()

	// close
	defer listener.Close()

	for {
		// accept
		fmt.Printf("start listen %s:%d\n", server.Ip, server.Port)
		conn, err := listener.Accept()
		if err != nil {
			fmt.Println("listener conn err: ", err)
			continue
		}
		// handle
		go server.Handler(conn)
	}
}
package main

import "net"

type User struct {
	Name string
	Addr string
	C    chan string
	Conn net.Conn
}

// NewUser 创建用户的API
func NewUser(conn net.Conn) *User {
	addr := conn.RemoteAddr().String()

	user := &User{
		Name: addr,
		Addr: addr,
		C:    make(chan string),
		Conn: conn,
	}
	// 初始化之后,就监听自己的这个channel,有消息就输出到客户端
	go user.ListenMessage()

	return user
}

func (user *User) ListenMessage() {
	for {
		msg := <-user.C
		user.Conn.Write([]byte(msg + "\n"))
	}
}

2.2. 结果展示

3. 用户消息广播功能

目前客户端只能被动接收消息,而不能主动发送消息,需要实现接收客户端的输入,再向其他用户广播

3.1. 代码

func (server *Server) Handler(conn net.Conn) {
	// fmt.Println("成功建立连接")
	// 用户上线,添加到OnlineMap
	user := NewUser(conn)
	server.mapLock.Lock()
	server.OnlineMap[user.Name] = user
	server.mapLock.Unlock()

	// 广播当前用户上线消息
	go server.BroadCast(user, "已上线")

	// 接收客户端消息
	go func() {
		buf := make([]byte, 4096)
		for {
			// 如果读到的数据是0,就说明是正常关闭的
			if n == 0 {
				server.BroadCast(user, "下线")
				return
			}
			// 以EOF结尾的
			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 去除用户信息的’\n‘
			msg := string(buf[:n-1])

			// 将这个消息进行广播
			server.BroadCast(user, msg)
		}
	}()

	// 阻塞当前的handler
	select {}
}

3.2. 结果展示

4. 用户业务封装

目前server里写了具体的user如何上线、下线、发消息,太耦合,应该移动到user代码里

4.1. 代码

// Handler 处理连接上线之后的操作
func (server *Server) Handler(conn net.Conn) {
	// fmt.Println("成功建立连接")
	// 用户上线,添加到OnlineMap
	user := NewUser(conn, server)
	user.Online()

	// 接收客户端消息
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			// 如果读到的数据是0,就说明是正常关闭的
			if n == 0 {
				user.Offline()
				return
			}
			// 以EOF结尾的
			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 去除用户信息的’\n‘
			msg := string(buf[:n-1])

			// 用户处理这个消息
			user.SendMsg(msg)
		}
	}()

	// 阻塞当前的handler
	select {}
}
package main

import "net"

type User struct {
	Name   string
	Addr   string
	C      chan string
	Conn   net.Conn
	Server *Server
}

// NewUser 创建用户的API
func NewUser(conn net.Conn, server *Server) *User {
	addr := conn.RemoteAddr().String()

	user := &User{
		Name:   addr,
		Addr:   addr,
		C:      make(chan string),
		Conn:   conn,
		Server: server,
	}
	// 初始化之后,就监听自己的这个channel,有消息就输出到客户端
	go user.ListenMessage()

	return user
}

func (user *User) ListenMessage() {
	for {
		msg := <-user.C
		user.Conn.Write([]byte(msg + "\n"))
	}
}

// Online 上线
func (user *User) Online() {
	user.Server.mapLock.Lock()
	user.Server.OnlineMap[user.Name] = user
	user.Server.mapLock.Unlock()

	// 广播当前用户上线消息
	go user.Server.BroadCast(user, "已上线")
}

// Offline 下线
func (user *User) Offline() {
	user.Server.mapLock.Lock()
	delete(user.Server.OnlineMap, user.Name)
	user.Server.mapLock.Unlock()
	// 广播当前用户下线消息
	go user.Server.BroadCast(user, "下线")
}

// SendMsg 处理消息
func (user *User) SendMsg(msg string) {
	go user.Server.BroadCast(user, msg)
}

5. 在线用户查询

当前用户查询有哪些用户在线,告知当前的客户端

5.1. 代码

// DoMsg 处理消息
func (user *User) DoMsg(msg string) {
	if msg == "who" {
		for _, onlineUser := range user.Server.OnlineMap {
			msg = "[" + onlineUser.Addr + "]" + onlineUser.Name + ":" + "在线...\n"
			user.SendMsg(msg)
		}
	} else {
		go user.Server.BroadCast(user, msg)
	}
}

// SendMsg 发消息给当前客户端
func (user *User) SendMsg(msg string) {
	user.Conn.Write([]byte(msg))
}

5.2. 结果展示

6. 修改用户名

上面时输入”who“触发查询在线用户的指令,下面来一个”rename|肖伟“

考虑重名禁止修改

6.1. 代码

func (user *User) DoMsg(msg string) {
	// 以rename|开头,Unicode字符结尾
	pattern := `^rename\|.*.$`
	reRename := regexp.MustCompile(pattern)
	if msg == "who" {
		for _, onlineUser := range user.Server.OnlineMap {
			msg = "[" + onlineUser.Addr + "]" + onlineUser.Name + ":" + "在线...\n"
			user.SendMsg(msg)
		}
	} else if reRename.MatchString(msg) {
		newName := strings.Split(msg, "|")[1]
		user.Server.mapLock.Lock()
		// 判断新用户名是否存在
		_, ok := user.Server.OnlineMap[newName]
		if ok {
			user.SendMsg("当前用户名已存在,请更换")
		} else {
			// 删除旧的
			delete(user.Server.OnlineMap, user.Name)
			user.Name = newName
			// 添加新的
			user.Server.OnlineMap[user.Name] = user
			user.Server.mapLock.Unlock()
			user.SendMsg("rename success to " + newName)
		}
	} else {
		go user.Server.BroadCast(user, msg)
	}
}

6.2. 结果展示

7. 超时强踢功能

用户长时间不活跃就踢掉,如何判断活跃?要求10s之内发消息,上线之后各自user开始计时,如果时间到10s了,就给当前用户的activeChannel添加信息,再有一个监听这个channel的goroutine去移除这类用户

上面打横线的是在user里做,应该是server做,server有什么,有handler方法,当客户端online之后,启动一个定时器,定时器结束之后执行移除的操作,当用户发送消息时,需要重置定时器,如何重置,再执行一次定时器就行,如何执行,for select的语句

注意这里user.Conn.Close()之后,还执行了fmt.Println("关闭了 ", user) fmt.Println("err, ", err),

关闭了  &{172.23.44.64:39320 172.23.44.64:39320 0xc000086120 0xc000066008 0xc00009c040}
err,  read tcp 172.23.32.1:8888->172.23.44.64:39320: use of closed network connection

也就是触发了一次Read

7.1. 代码

func (server *Server) Handler(conn net.Conn) {
	// fmt.Println("成功建立连接")
	// 用户上线,添加到OnlineMap
	user := NewUser(conn, server)
	user.Online()

	isLive := make(chan bool)
	// 接收客户端消息
	go func() {
		buf := make([]byte, 4096)
		for {
			n, err := conn.Read(buf)
			// 如果读到的数据是0,就说明是正常关闭的
			if n == 0 {
                fmt.Println("关闭了 ", user)
				fmt.Println("err, ", err)
				user.Offline()
				return
			}
			// 以EOF结尾的
			if err != nil && err != io.EOF {
				fmt.Println("Conn Read err:", err)
				return
			}

			// 去除用户信息的’\n‘
			msg := string(buf[:n-1])

			// 用户处理这个消息
			user.DoMsg(msg)
			// 活跃了
			isLive <- true
		}
	}()

	// 阻塞当前的handler
	for {
		select {
		case <-isLive:
			// doNothing,只是为了会顺序执行下面的定时器,起到重置的作用
		case <-time.After(10 * time.Second):
			// 移除当前用户
			server.mapLock.Lock()
			delete(server.OnlineMap, user.Name)
			server.mapLock.Unlock()
			user.SendMsg("你已超时,强制下线")
			// 关闭用户聊天的channel
			close(user.C)
			// 断开连接
			user.Conn.Close()
			return
		}
	}
}

7.2. 结果展示

8. 私聊功能

也得定义发消息的格式,比如”to|棉花泡泡糖|我是张三“

用户发之前先用who看看,系统发之前要检查用户是否存在

如何把消息发给指定的用户呢?onlineMap里面取

8.1. 代码

func (user *User) DoMsg(msg string) {
	// 以rename|开头,Unicode字符结尾
	patternRename := `^rename\|.*.$`
	reRename := regexp.MustCompile(patternRename)

	// 私聊消息的正则
	patternPrivateChat := `^to\|.*.\|.*.$`
	rePrivateChat := regexp.MustCompile(patternPrivateChat)

	if msg == "who" {
		for _, onlineUser := range user.Server.OnlineMap {
			msg = "[" + onlineUser.Addr + "]" + onlineUser.Name + ":" + "在线...\n"
			user.SendMsg(msg)
		}
	} else if reRename.MatchString(msg) {
		newName := strings.Split(msg, "|")[1]
		user.Server.mapLock.Lock()
		// 判断新用户名是否存在
		_, ok := user.Server.OnlineMap[newName]
		if ok {
			user.SendMsg("当前用户名已存在,请更换")
		} else {
			// 删除旧的
			delete(user.Server.OnlineMap, user.Name)
			user.Name = newName
			// 添加新的
			user.Server.OnlineMap[user.Name] = user
			user.Server.mapLock.Unlock()
			user.SendMsg("rename success to " + newName)
		}
	} else if rePrivateChat.MatchString(msg) {
		userMsg := strings.Split(msg, "|")
		toUserName, theMsg := userMsg[1], userMsg[2]
		toUser, ok := user.Server.OnlineMap[toUserName]
		if !ok {
			user.SendMsg("当前用户不在线,请重新选择用户")
			return
		}
		if len(theMsg) == 0 {
			user.SendMsg("信息为空,请重新发送")
			return
		}
		// 发给指定用户
		toUser.SendMsg("[" + user.Addr + "]" + user.Name + ":" + theMsg)
	} else {
		go user.Server.BroadCast(user, msg)
	}
}

8.2. 结果展示

李四给张三互发消息,没人理棉花泡泡糖

9. 客户端实现建立连接

不使用nc命令,而是通过一个客户端,内部是net.Dial()去建立连接

9.1. 代码

package main

import (
	"flag"
	"fmt"
	"net"
)

type Client struct {
	ServerIp   string
	ServerPort int
	Name       string
	Conn       net.Conn
}

func NewClient(serverIp string, serverPort int) *Client {
	// 创建客户端对象
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
	}
	// 连接到server
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	if err != nil {
		fmt.Println("net.Dial err, ", err)
		return nil
	}

	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 main() {
    // 命令行解析
	flag.Parse()
	client := NewClient(serverIp, serverPort)
	if client == nil {
		fmt.Println(">>>>>连接失败...")
		return
	}

	fmt.Println(">>>>>连接成功...")

	// 阻塞
	for {
	}
}

9.2. 展示结果

10. 菜单显示

判断用户的意图:公聊、私聊、更新用户名、退出系统

10.1. 代码

package main

import (
	"flag"
	"fmt"
	"net"
)

type Client struct {
	ServerIp   string
	ServerPort int
	Name       string
	Conn       net.Conn
	clientFlag int // 当前客户端的模式
}

func NewClient(serverIp string, serverPort int) *Client {
	// 创建客户端对象
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
		clientFlag: 999,
	}
	// 连接到server
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	if err != nil {
		fmt.Println("net.Dial err, ", err)
		return nil
	}

	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 clientFlag int
	fmt.Println("1.公聊模式")
	fmt.Println("2.私聊模式")
	fmt.Println("3.更改用户名")
	fmt.Println("0.退出")

	fmt.Scanln(&clientFlag)

	if clientFlag >= 0 && clientFlag <= 3 {
		client.clientFlag = clientFlag
		return true
	} else {
		fmt.Println("请输入合法范围内的数字")
		return false
	}
}

func (client *Client) run() {
	for client.clientFlag != 0 {
		for client.menu() != true {
		}
		switch client.clientFlag {
		case 1:
			// 公聊模式
			fmt.Println("选择了公聊模式")
			break
		case 2:
			// 私聊模式
			fmt.Println("选择了私聊模式")
			break
		case 3:
			// 更改用户名
			fmt.Println("选择了更改用户名")
			break
		}
	}
}

func main() {
	flag.Parse()
	client := NewClient(serverIp, serverPort)
	if client == nil {
		fmt.Println(">>>>>连接失败...")
		return
	}

	fmt.Println(">>>>>连接成功...")

	client.run()
}

10.2. 展示结果

11. 菜单里的功能实现

公聊模式

私聊模式

更改用户名

打印server响应的回执

// 把client的消息拷贝到stdout输出上,永久阻塞监听
io.Copy(os.Stdout, client.Conn)

11.1. 代码

package main

import (
	"flag"
	"fmt"
	"io"
	"net"
	"os"
)

type Client struct {
	ServerIp   string
	ServerPort int
	Name       string
	Conn       net.Conn
	clientFlag int // 当前客户端的模式
}

func NewClient(serverIp string, serverPort int) *Client {
	// 创建客户端对象
	client := &Client{
		ServerIp:   serverIp,
		ServerPort: serverPort,
		clientFlag: 999,
	}
	// 连接到server
	conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", serverIp, serverPort))
	if err != nil {
		fmt.Println("net.Dial err, ", err)
		return nil
	}

	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 clientFlag int
	fmt.Println("1.公聊模式")
	fmt.Println("2.私聊模式")
	fmt.Println("3.更改用户名")
	fmt.Println("0.退出")

	fmt.Scanln(&clientFlag)

	if clientFlag >= 0 && clientFlag <= 3 {
		client.clientFlag = clientFlag
		return true
	} else {
		fmt.Println("请输入合法范围内的数字")
		return false
	}
}

func (client *Client) PublicChat() {
	fmt.Println("选择了公聊模式")
	fmt.Println("请输入消息内容,输入exit退出")
	var msg string
	fmt.Scanln(&msg)

	for msg != "exit" {
		if len(msg) == 0 {
			fmt.Println("不能发送空消息")
			fmt.Scanln(&msg)
			continue
		}
		_, err := client.Conn.Write([]byte(msg + "\n"))
		if err != nil {
			fmt.Println("conn write err, ", err)
			break
		}
		// 置空,方便下次输入
		msg = ""
		fmt.Scanln(&msg)
	}
}

func (client *Client) ListOnlineUser() {
	_, err := client.Conn.Write([]byte("who\n"))
	if err != nil {
		fmt.Println("conn write err, ", err)
	}
}

func (client *Client) PrivateChat() {
	fmt.Println("选择了私聊模式")
	client.ListOnlineUser()
	fmt.Println("请输入要聊天的对象[用户名],exit退出")
	var toUserName string
	fmt.Scanln(&toUserName)

	for toUserName != "exit" {
		fmt.Println("请输入消息内容,exit退出")
		var msg string
		fmt.Scanln(&msg)
		for msg != "exit" {
			if len(msg) == 0 {
				fmt.Println("不能发送空消息")
				fmt.Scanln(&msg)
				continue
			}
			sendMsg := "to|" + toUserName + "|" + msg
			_, err := client.Conn.Write([]byte(sendMsg + "\n"))
			if err != nil {
				fmt.Println("conn write err, ", err)
				break
			}
			// 置空,方便下次输入
			msg = ""
			fmt.Scanln(&msg)
		}
		client.ListOnlineUser()
		fmt.Println("请输入要聊天的对象[用户名],exit退出")
		fmt.Scanln(&toUserName)
	}
}

func (client *Client) Rename() {
	fmt.Println("请输入用户名")
	fmt.Scanln(&client.Name)

	// 模拟手动输入这个协议
	sendMsg := "rename|" + client.Name
	_, err := client.Conn.Write([]byte(sendMsg + "\n"))
	if err != nil {
		fmt.Println("conn write err, ", err)
	}
}

func (client *Client) run() {
	for client.clientFlag != 0 {
		for client.menu() != true {
		}
		switch client.clientFlag {
		case 1:
			// 公聊模式
			client.PublicChat()
			break
		case 2:
			// 私聊模式
			client.PrivateChat()
			break
		case 3:
			// 更改用户名
			client.Rename()
			break
		}
	}
}

// DealResponse 显示返回结果,显示到标准输出
func (client *Client) DealResponse() {
	// 把client的消息拷贝到stdout输出上,永久阻塞监听
	io.Copy(os.Stdout, client.Conn)
}

func main() {
	flag.Parse()
	client := NewClient(serverIp, serverPort)
	if client == nil {
		fmt.Println(">>>>>连接失败...")
		return
	}

	fmt.Println(">>>>>连接成功...")

	// 开启一个goroutine去处理server的回执消息
	go client.DealResponse()

	// 客户端业务
	client.run()
}

11.2. 展示结果

12. 优化超时强踢导致CPU占用率高,读取关闭的channel

第七点超时强踢功能里,此处关闭 user.C,关闭 user.Conn,然后return

case <-time.After(10 * time.Second):
    // 从OnlineMap移除当前用户...
    user.SendMsg("你已超时,强制下线")
    // 关闭用户聊天的channel
    close(user.C)
    // 断开连接
    user.Conn.Close()
    return

下面是用户初始化之后会执行的 goroutine,user.C 已经关闭了,读取到的 msg 是对应 channel 的零值,空字符串,然后就会走到下面的 user.Conn.Write(),但是上面已经 user.Conn.Close() 了,所以这里会报错 connection closed。这里是 for 循环,所以 err 会一直输出,导致 CPU 占有率很高。

所以我们要让 ListenMessage 知道当前的 user.C 已经关闭了,就不要再读了,也就不会走下面的 write,

func (user *User) ListenMessage() {
	for {
		msg := <-user.C
		// 当超时下线的时候,这里user.C已经关闭了,取到的msg是零值。
		// 并且下线的时候user.Conn已经close了,下面的Conn.Write就会报错
		// 所以得让ListenMessage知道这个conn关闭了,或者是channel已经关闭了,就不要执行下面的了
		_, err := user.Conn.Write([]byte(msg + "\n"))
		if err != nil {
			// 会一直输出err
			fmt.Println("conn write err, ", err)
			//return
		}
	}
}

利用到的特性是 golang 里面的 for msg := range ch {},当 ch 没了,for 循环就退出

for msg := range user.C {
    _, err := user.Conn.Write([]byte(msg + "\n"))
    if err != nil {
        fmt.Println("conn write err, ", err)
    }
}

13. 复盘

如何一步一步迭代开发搭建起来的?

  1. 完成一个server,能通过TCP协议建立连接
  2. 客户端上线之后能有提醒,server能记录在线的客户端,能通知其他客户端,需要有广播
  3. 上面完成了上线提醒的广播,这是server做的,用户自己本身也需要发消息,所以涉及到socket读取控制台的数据 conn.read
  4. 封装用户业务功能,让user和server分离,user对象需要加一个server属性
  5. 查询在线的用户,约定一个协议,输入who,就是触发这个动作
  6. 修改当前用户的用户名,也是约定协议,输入rename|xw
  7. 长期不活跃的用户被剔除,需要有定时器,在用户发消息之后,需要重置定时器,用for select
  8. 私聊发送消息,依据用户名从server获取到user对象,然后发消息,也需要约定协议
  9. 现在都是nc连接的,下面实现一个客户端,模拟输入的约定消息,flag去读取命令行的输入

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值