前言
以前从来没有开发过像聊天室这样的通讯项目,所以刚开始写的时候有点茫然,在百度上搜了大量的博客,我不太喜欢看视频。发现很多博主写的都太简单了,功能只有群发消息,私发。还有一些写的很好大事涉及到了go的框架,还有就是使用了redis,不是我说很少见写聊天室使用mysql的,我的百度搜的就是这些。最终结合视频(还是要看视频)以及自己的筛选,写了这个聊天室。我准备分两篇来写。
功能介绍
1.服务端添加了管理员,管理员可以屏蔽某个人,给某个人私发消息,群发通知,把某人踢出群聊 2.客户端主要是群发,私发, 3.还有管理员,用户的登录注册,这里没用mysql,用的是txt文件存的注册用户。
上面三幅图是我服务端以及两个客户端的运行界面,主要的运行效果都在上面
咱们先说说客户端的功能实现
数据的输入
func inputMessage() {
var input, objUser string
for {
fmt.Scanln(&input)
switch input {
// 用户退出
case "/quit":
fmt.Println("退出聊天室,欢迎下次使用!")
os.Exit(0)
// 向单个用户发送消息
case "/to":
fmt.Println("请输入聊天对象:")
fmt.Scanln(&objUser)
fmt.Println("请输入消息:")
fmt.Scanln(&input)
if len(input) != 0 {
input = "To-" + uname + "-" + objUser + "-" + input
}
// 默认群发
default:
if len(input) != 0 {
input = uname + " : " + input
}
}
// 发送数据
if len(input) != 0 {
if !isSheild {
sentData <- input
} else {
fmt.Println("你已被管理员屏蔽,无法发言!")
}
input = ""
}
}
}
登录的实现
func clientLog() {
LOOP:
for {
var username, password string
fmt.Println("请输入用户名:")
fmt.Scanln(&username)
if username == "" {
goto LOOP
}
fmt.Println("请输入密码:")
fmt.Scanln(&password)
sentData <- "Log-" + username + "-" + password
res := <-recvData
if res == "success" {
fmt.Println("登录成功,你已进入聊天室!")
uname = username // 登录成功保存用户名
go showMassage() // 启动聊天内容显示协程
break
} else {
fmt.Println(res)
}
}
inputMessage() // 启动聊天内容输入
}
注册的实现
func clientReg() {
for {
var username, pwd, pwdCheck string
fmt.Println("请输入用户名:")
fmt.Scanln(&username)
fmt.Println("请输入密码:")
fmt.Scanln(&pwd)
fmt.Println("请确认密码:")
fmt.Scanln(&pwdCheck)
if pwdCheck != pwd {
fmt.Println("两次输入密码不一致,请重新注册!")
} else {
sentData <- ("Reg-" + username + "-" + pwd) // 发送用户名和密码
res := <-recvData // 等待登录结果
if res == "success" {
fmt.Println("注册成功,请登录")
break
} else {
fmt.Println(res)
}
}
}
clientLog() // 转至登录
}
创建tcp链接 tcp链接是客户端与服务端相连的一种形式,通过两者相同的端口号来进行交互
func createTCP(tcpaddr string) net.Conn {
tcpAddr, err := net.ResolveTCPAddr("tcp4", tcpaddr)
checkError(err, "ResolveTCPAddr")
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err, "DialTCP")
fmt.Println("连接服务器成功!请输入将要进行的操作:1、登录 2、注册")
return conn
}
客户端的启动程序
func startClient(tcpAddr string) {
conn = createTCP(tcpAddr) // 创建TCP连接
go recv(recvData, conn) // 启动数据接收协程
go sent(sentData, conn) // 启动数据发送协程
LOOP:
{
var input string
fmt.Scanln(&input)
if input == "1" {
clientLog() // 登录
} else if input == "2" {
clientReg() // 注册
} else {
fmt.Println("输入有误,请重新输入!")
goto LOOP
}
}
}
数据接收和数据发送
func recv(recvData chan string, conn net.Conn) {
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
checkError(err, "Connection")
recvData <- string(buf[:n])
}
}
/**
* 数据发送
* sentData 接收数据Channel
* conn TCP连接对象
**/
func sent(sentData chan string, conn net.Conn) {
for {
data := <-sentData
_, err := conn.Write([]byte(data))
checkError(err, "Connection")
}
}
主程序
func main() {
if len(os.Args) == 2 {
startClient(os.Args[1]) // 启动客户端
} else {
fmt.Println("输入错误!")
}
}
客户端引用的标准库
import (
"fmt"
"net"
"os"
)
客户端的代码实现就是以上部分,具体的解释都在注释里
前言
前一篇说了聊天室客户端的实现,这一篇就接着上篇讲服务端,与很多聊天室不同,我在服务端里加入了管理员,管理员与普通用户的功能相比多了屏蔽发言,以及踢出群聊的功能
数据的接收与发送 和客户端的没什么不同
func recv(recvData chan string, conn net.Conn) {
for {
buf := make([]byte, 1024)
n, err := conn.Read(buf)
if err != nil {
connection <- conn
return
}
recvData <- string(buf[:n])
}
}
/**
* 数据发送
* sentData 接收数据Channel
* conn TCP连接对象
**/
func sent(sentData chan string, conn net.Conn) {
for {
data := <-sentData
_, err := conn.Write([]byte(data))
if err != nil {
connection <- conn
return
}
}
}
客户端的连接管理
func connManager(connection chan net.Conn) {
for {
conn := <-connection
username := ipToUname[conn.RemoteAddr().String()]
notesInfo(messages, "用户:"+username+"已退出聊天室!")
conn.Close()
delete(conns, username)
delete(ipToUname, conn.RemoteAddr().String())
}
}
数据解析与封装
func dataSent(conns *map[string]clientInfo, messages chan string) {
for {
msg := <-messages
fmt.Println(msg)
data := strings.Split(msg, "-") // 聊天数据分析:
length := len(data)
if length == 2 { // 管理员单个用户发送控制命令
(*conns)[data[0]].sentData <- data[1]
} else if length == 3 { // 用户列表
(*conns)[data[1]].sentData <- data[2]
} else if length == 4 { // 向单个用户发送数据
msg = data[1] + " say to you : " + data[3]
(*conns)[data[2]].sentData <- msg
} else {
// 群发
for _, value := range *conns {
value.sentData <- msg
}
}
}
}
管理员注册处理
/**
* 获取全部已注册用户的数据,用于登录注册时验证
*/
func getAllUser(filename string) map[string]userData {
buf := make([]byte, dataSize)
udata := make(map[string]userData)
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0766) //打开保存用户的文件
defer file.Close() //保证文件关闭
errorExit(err, "getAllUser")
n, _ := file.Read(buf)
json.Unmarshal(buf[:n], &udata) //将流文件转成json并赋值给udata这个map
return udata
}
/**
* 添加新注册用户数据
*/
func insertNewUser(filename, username, password string) {
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0766)
defer file.Close()
checkError(err, "insertNewUser")
uData[username] = userData{password, 1}
data, _ := json.MarshalIndent(uData, "", " ")
file.WriteString(string(data))
}
func regProcess(username, password string) (status bool, info string) {
if len(uData) == maxReg {
return false, "注册人数已满!"
}
if _, ok := uData[username]; ok {
return false, "用户已存在,请更换注册名!"
} else {
insertNewUser(dataFileName, username, password)
return true, success
}
}
管理员登录处理
func logProcess(username, password string) (status bool, info string) {
if len(conns) == maxLog {
return false, "当前登录人数已满,请稍后登录"
}
if user, ok := uData[username]; !ok {
return false, "用户名或密码错误!"
} else {
if user.Password == password {
if _, ok := conns[username]; ok {
return false, "用户已登录!"
} else {
return true, success
}
} else {
return false, "用户名或密码错误!"
}
}
}
用户进入聊天室资格认证
func userAuth(conn *net.Conn, recvData, sentData chan string) {
for {
data := strings.Split(<-recvData, "-") // 等待用户发送登录或注册数据
flag, username, password := data[0], data[1], data[2]
if flag == "Reg" {
_, info := regProcess(username, password)
sentData <- info
} else {
status, info := logProcess(username, password)
sentData <- info
if status == true {
messages <- ("用户:" + username + "进入聊天室") // 用户登录成功,发送系统通知
conns[username] = clientInfo{*conn, sentData} // 记录用户登录状态信息
ipToUname[(*conn).RemoteAddr().String()] = username // 记录IP与用户名对应信息
messages <- "List-" + username + "-" + userList() // 向用户发送已在线用户列表
go dataRec(&conns, username, recvData, messages) // 开启服务器对该客户端数据接收线程
break
}
}
}
}
服务端的tcp连接创建
func createTCP(tcpaddr string) *net.TCPListener {
tcpAddr, err := net.ResolveTCPAddr("tcp4", tcpaddr)
errorExit(err, "ResolveTCPAddr")
l, err := net.ListenTCP("tcp", tcpAddr)
errorExit(err, "DialTCP")
return l
}
管理员的登录注册
func intoManager() {
fmt.Println("请输入将要进行操作:1、管理员注册 2、管理员登录")
var input string
LOOP:
{
fmt.Scanln(&input)
switch input {
case "1":
adminReg()
case "2":
adminLog()
default:
goto LOOP
}
}
admimManager(messages)
}
/**
* 管理员登录程序
**/
func adminLog() {
for {
var adminname, password string
fmt.Println("请输入管理员用户名:")
fmt.Scanln(&adminname)
fmt.Println("请输入管理员密码:")
fmt.Scanln(&password)
if pwd, ok := adminList[adminname]; !ok {
fmt.Println("用户名或者密码错误")
} else {
if pwd != password {
fmt.Println("用户名或者密码错误!")
} else {
fmt.Println("登录成功!")
break
}
}
}
}
/**
* 管理员注册程序
**/
func adminReg() {
var adminname, password string
fmt.Println("请输入管理员用户名:")
fmt.Scanln(&adminname)
fmt.Println("请输入管理员密码:")
fmt.Scanln(&password)
adminList[adminname] = password //将注册的管理员姓名密码保存到adminList中,单次启动有效
fmt.Println("注册成功!请登录")
adminLog() //跳转到登录
}
管理员一系列功能实现
func admimManager(messages chan string) {
for {
var input, objUser string
fmt.Scanln(&input)
switch input {
case "/to":
fmt.Println(userList())
fmt.Println("请输入聊天对象:")
fmt.Scanln(&objUser)
if _, ok := conns[objUser]; !ok {
fmt.Println("不存在此用户!")
} else {
fmt.Println("请输入消息:")
fmt.Scanln(&input)
notesInfo(messages, "To-Manager-"+objUser+"-"+input)
}
case "/all":
fmt.Println("请输入消息:")
fmt.Scanln(&input)
notesInfo(messages, "Manager say : "+input)
case "/shield":
fmt.Println(userList())
fmt.Println("请输入屏蔽用户名:")
fmt.Scanln(&objUser)
notesInfo(messages, objUser+"-/shield")
notesInfo(messages, "用户:"+objUser+"已被管理员禁言!")
case "/remove":
fmt.Println(userList())
fmt.Println("请输入踢出用户名:")
fmt.Scanln(&objUser)
notesInfo(messages, "用户:"+objUser+"已被管理员踢出聊天室!")
if _, ok := conns[objUser]; !ok {
fmt.Println("不存在此用户!")
} else {
conns[objUser].conn.Close() // 删除该用户的连接
delete(conns, objUser) // 从已登录的列表中删除该用户
}
}
}
}
服务端启动程序
func StartServer(port string) {
l := createTCP(":" + port)
fmt.Println("服务端启动成功,正在监听端口!")
go dataSent(&conns, messages) // 启动服务器广播线程
go intoManager() // 启动管理模块
go connManager(connection) // 启动连接管理线程
for {
conn, err := l.Accept()
if checkError(err, "Accept") == false {
continue
}
fmt.Println("客户端:", conn.RemoteAddr().String(), "连接服务器成功!")
var recvData = make(chan string)
var sentData = make(chan string)
go recv(recvData, conn) // 开启对客户端的接受数据线程
go sent(sentData, conn) // 开启对客户端的发送数据线程
go userAuth(&conn, recvData, sentData) // 用户资格认证
}
}
服务端主程序
func main() {
if len(os.Args) == 2 {
uData = getAllUser(dataFileName) // 用户登录、注册数据初始化
StartServer(os.Args[1]) // 启动客户端
} else {
fmt.Println("输入错误!")
}
}
用到的标准库
import (
"encoding/json"
"fmt"
"net"
"os"
"strings"
)
总结 终于写完了,啊啊啊! 源码地址 gitee.com/qian-xiawud…