initial commit

This commit is contained in:
2018-12-15 00:34:15 +08:00
parent c43d9a1a75
commit 7b06ef8dea
18 changed files with 11257 additions and 1 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
#*.dll
*.so
*.dylib
+9551
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+60
View File
@@ -0,0 +1,60 @@
package main
//宏
const (
//最大包头长度
max_head_length uint32 = 4096 //4K
//最大消息长度
max_message_length uint32 = 0x6400000 //64M
//最大头像大小
max_photo_size uint32 = 0x400000 //4M
//服务器地址
server_addr string = "mattuy.top:3727"
)
//用户组
const (
ugroup_user string = "User" //普通用户
ugroup_vip string = "Vip" //会员
ugroup_admin string = "Admin" //管理员
ugroup_group string = "Group" //群聊
)
//消息头的通用字段名称
const (
keyname_operation string = "Operation" //操作名
keyname_token string = "Token"
)
//控制消息
const (
operation_panic string = "Panic"
operation_trans_message string = "TransMessage"
operation_send_message string = "SendMessage"
operation_ping string = "Ping"
operation_login string = "Login"
operation_logout string = "Logout"
operation_offline string = "Offline"
operation_get_user_list string = "GetUserList"
operation_update_userinfo string = "UpdateUserInfo"
)
//帮助
const help_message string = `
欢迎使用StormChat!
输入“help”查看命令列表
输入“.”退出命令模式,退出后可输入“.”重新进入命令模式
聊天时可在行末输入‘\’以换行输入消息
`
//指令列表
const command_list string = `
指令列表:
. 退出命令模式 login 登录
to 设定聊天对象 logout 注销
friends 查看用户列表 help 显示命令列表
scroll 消息面板翻页 clear 清空消息记录
exit 退出程序
CTRL + C 强制退出
`
+256
View File
@@ -0,0 +1,256 @@
package main
import (
"bufio"
"fmt"
"os"
"strings"
"time"
)
/*
#include <conio.h>
#include <stdlib.h>
int getkey() {
return getch();
}
*/
import "C"
var commandMode bool //指示当前是否处于命令模式
func main() {
defer func() {
fmt.Print("按任意键退出程序")
C.getkey()
DestroyConsoleWindow()
}()
InitConsole()
client := NewSession()
if client == nil {
return
}
SetUserNameText("未登录")
SetCurrentFriendText("")
PrintInfo(help_message)
if len(os.Args) >= 3 {
client.Login(os.Args[1], os.Args[2])
commandMode = false
}
commandMode = true
PrintInfo(command_list)
if InputCommand(client) {
return
}
var message string
var input string
for {
if message == "" {
ClearInput()
}
reader := bufio.NewReader(os.Stdin)
input, _ = reader.ReadString('\n')
input = strings.TrimSuffix(input, "\r\n")
input = strings.TrimSuffix(input, "\n")
switch input {
case ".":
message = ""
if InputCommand(client) {
return
}
case ".quit":
return
case ".exit":
return
default:
message += input
if strings.HasSuffix(input, "\\") {
message = strings.TrimSuffix(message, "\\")
message += "\r\n"
} else {
client.SendMessage([]byte(message))
message = ""
} //发送成功,清空消息缓存
}
}
}
//输入指令
func InputCommand(client *Session) bool {
defer SetOperationText("聊天")
SetOperationText("命令模式")
for {
ClearInput()
var input string
reader := bufio.NewReader(os.Stdin)
input, _ = reader.ReadString('\n')
input = strings.TrimSuffix(input, "\r\n")
input = strings.TrimSuffix(input, "\n")
switch strings.TrimRight(input, " ") {
case "scroll":
ScrollMessage()
case "login":
success := LoginWithInput(client)
if !success {
client.destroy()
client = NewSession()
PrintError("登录失败,请重试。")
} //如果连接已中断则重新创建session
case "logout":
client.Logout()
case "to":
SwitchReceiver(client)
case "friends":
ViewFriends(client)
case "clear":
ClearOutput()
case "help":
PrintInfo(command_list)
case ".":
return false
case "exit":
return true
default:
PrintError("无效命令")
}
}
}
//发起登录。如果返回值为false则session已失效(根据发送数据失败判断)
func LoginWithInput(session *Session) bool {
SetOperationText("登录")
defer func() {
if commandMode {
SetOperationText("命令模式")
} else {
SetOperationText("聊天")
}
}()
if session.status == session_status_running {
PrintError("已经登录,请先注销。")
return true //虽然登录失败但连接仍有效,因此返回true
}
reader := bufio.NewReader(os.Stdin)
PrintInputTip("user: ")
user, _ := reader.ReadString('\n')
user = strings.TrimSuffix(user, "\r\n")
user = strings.TrimSuffix(user, "\n")
PrintInputTip("password: ")
pwd, _ := reader.ReadString('\n')
pwd = strings.TrimSuffix(pwd, "\r\n")
pwd = strings.TrimSuffix(pwd, "\n")
if !session.Login(user, pwd) {
return false
}
return true
}
//更换聊天对象
func SwitchReceiver(session *Session) {
if session.status != session_status_running {
PrintError("请先登录")
return
}
SetOperationText("切换好友")
PrintInputTip("好友ID或昵称(输入*启用广播模式): ")
var found bool = false //好友是否存在
var friend string
reader := bufio.NewReader(os.Stdin)
friend, _ = reader.ReadString('\n')
friend = strings.TrimSuffix(friend, "\r\n")
friend = strings.TrimSuffix(friend, "\n")
if friend == "*" {
SetCurrentFriendText("广播模式")
session.receiver = friend
} //广播消息
//查询好友列表
for _, item := range session.friends {
if strings.ToLower(item.NickName) == strings.ToLower(friend) || item.User == friend {
SetCurrentFriendText(item.NickName)
session.receiver = item.User
found = true
break
}
}
if commandMode {
SetOperationText("命令模式")
} else {
SetOperationText("聊天")
}
if !found {
PrintError("未找到名为【" + friend + "】的好友")
}
}
//查看好友列表
func ViewFriends(session *Session) {
list := session.friends
if session.status != session_status_running {
PrintError("请先登录")
return
}
if len(list) == 0 {
PrintInfo("好友列表为空")
return
}
for _, item := range list {
PrintInfo(item.User + " " + item.NickName)
}
}
//滚动消息面板
func ScrollMessage() {
if commandMode {
defer SetOperationText("命令模式")
} else {
defer SetOperationText("聊天")
}
defer ClearInput()
SetOperationText("翻页模式")
PrintInputTip("上翻:P\n下翻:N\n退出翻页:ESC\n")
for {
key := C.getkey()
switch key {
case 'p':
ScrollOutputArea(-1)
case 'n':
ScrollOutputArea(1)
case 27:
return
}
}
}
//输出错误
func PrintError(err string) {
PrintOutputLine("[ERROR] "+err, FOREGROUND_RED)
}
//输出信息
func PrintInfo(text string) {
PrintOutputLine(text, FOREGROUND_RED|FOREGROUND_GREEN)
}
func PrintLog(str string) {
PrintOutputLine("[INFO] "+str, FOREGROUND_INTENSITY)
}
//输出消息头
func PrintMessageHead(head string, intensity bool) {
if intensity {
PrintOutputLine(head, FOREGROUND_INTENSITY)
} else {
PrintOutputLine(head, FOREGROUND_GREEN)
}
}
//输出消息体
func PrintMessage(msg string) {
PrintOutputLine(msg, FORGROUND_WHITE)
}
//构造消息头
func MakeHead(operation string) map[string]string {
headInfo := make(map[string]string)
headInfo[keyname_token] = string(time.Now().UnixNano())
headInfo[keyname_operation] = operation
return headInfo
}
+324
View File
@@ -0,0 +1,324 @@
package main
import (
"encoding/binary"
"encoding/json"
"net"
"sync/atomic"
"time"
)
//当前session的状态
const (
session_status_created = iota //已创建,但未登录
session_status_running // 通道已建立,session正常运行
session_status_stoped // 标识连接已断开
session_status_destroyed
)
type Session struct {
sender *UserInfo //消息发送者
receiver string //消息接收者
friends []UserInfo //好友列表
conn *net.TCPConn //TCP连接
status int32 //session运行状态
stopChan chan bool //Session终止标识,传递数据即终止。仅由读消息循环用于终止写循环
writeCh chan []byte //写消息通道
writeResultCh chan bool //是否写成功
}
//初始化
func NewSession() *Session {
tcpAddr, _ := net.ResolveTCPAddr("tcp", server_addr)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
if err != nil {
PrintError("连接服务器失败")
return nil
}
var session = new(Session)
session.sender = nil
session.receiver = ""
session.status = session_status_created
session.stopChan = make(chan bool)
session.writeCh = make(chan []byte)
session.writeResultCh = make(chan bool)
session.conn = conn
go session.SendLoop()
go session.ReceiveLoop()
return session
}
//销毁聊天连接
func (session *Session) destroy() {
if session.status == session_status_destroyed {
return
}
session.status = session_status_destroyed
session.conn.Close()
close(session.writeCh)
close(session.writeResultCh)
}
//消息写循环
func (session *Session) SendLoop() {
defer session.destroy()
for {
select {
case data, ok := <-session.writeCh:
if !ok {
atomic.SwapInt32(&session.status, session_status_stoped)
continue
}
writeLen, err := session.conn.Write(data)
if err != nil || (writeLen != len(data)) {
PrintError(err.Error())
atomic.SwapInt32(&session.status, session_status_stoped)
session.writeResultCh <- false
} //写失败
session.writeResultCh <- true
case <-session.stopChan:
return
}
}
}
//消息读循环
func (session *Session) ReceiveLoop() {
defer func() {
session.stopChan <- true
}() //终止写循环
var err error //错误
for {
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return
} //检查session是否已终止
//读取数据
head := session.readDataWithLength()
data := session.readDataWithLength()
if head == nil || data == nil {
PrintError("获取数据失败")
atomic.SwapInt32(&session.status, session_status_stoped)
continue
}
//解析包头
headInfo := make(map[string]string)
err = json.Unmarshal(head, &headInfo)
if err != nil {
PrintError("解析包头失败")
continue
}
switch headInfo[keyname_operation] {
case operation_send_message:
session.ResultHandler(headInfo)
break
case operation_update_userinfo:
session.ResultHandler(headInfo)
break
case operation_offline:
case operation_panic:
session.PanicHandler(headInfo)
break
case operation_login:
session.LoginDoneHandler(headInfo, data)
break
case operation_trans_message:
session.MessageHandler(headInfo, data)
break
case operation_get_user_list:
session.GetUserListHandler(headInfo, data)
break
default:
continue
}
}
}
//处理请求结果反馈消息
func (session *Session) ResultHandler(headInfo map[string]string) {
if headInfo["Error"] == "" {
PrintLog(headInfo[keyname_operation] + " successfully.")
} else {
PrintError("Failed to " + headInfo[keyname_operation] + ": " + headInfo["Error"])
}
}
//服务器报错
func (session *Session) PanicHandler(headInfo map[string]string) {
PrintError("Server Error: " + headInfo["Error"])
atomic.SwapInt32(&session.status, session_status_stoped)
}
//登录反馈
func (session *Session) LoginDoneHandler(headInfo map[string]string, data []byte) {
session.ResultHandler(headInfo)
if headInfo["Error"] == "" {
user := new(UserInfo)
err := json.Unmarshal(data, user)
if err != nil {
PrintError("解析用户信息失败")
return
} else {
session.sender = user
SetUserNameText(user.NickName)
}
atomic.SwapInt32(&session.status, session_status_running)
session.GetUserList()
}
}
//获取用户列表
func (session *Session) GetUserListHandler(headInfo map[string]string, data []byte) {
session.ResultHandler(headInfo)
if headInfo["Error"] != "" {
return
}
err := json.Unmarshal(data, &session.friends)
if err != nil {
PrintError("解析用户信息失败: " + err.Error())
}
}
//输出消息
func (session *Session) MessageHandler(headInfo map[string]string, message []byte) {
if headInfo["Error"] != "" {
PrintError("发送消息失败: " + headInfo["Error"])
return
}
var user *UserInfo = nil
for _, item := range session.friends {
if item.User == headInfo["From"] {
user = &item
break
}
}
when, _ := time.Parse(time.RFC3339, headInfo["When"])
if user == nil {
PrintMessageHead(headInfo["From"]+" ["+when.Format("2006-01-02 15:04:05")+"]", false)
} else {
PrintMessageHead(user.NickName+" ["+when.Format("2006-01-02 15:04:05")+"]", false)
}
PrintMessage(string(message))
if session.receiver == "" && user != nil {
session.receiver = user.User
SetCurrentFriendText(user.NickName)
} //如果无当前聊天对象则将聊天对象设置为消息发送者
}
//发送数据
func (session *Session) send(head []byte, data []byte) bool {
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
var buf = make([]byte, 4) //长度标识
var packet = make([]byte, 0, len(head)+len(data)+4+4) //headLen + head + msgLen + data
//写包头
//取包头长
binary.BigEndian.PutUint32(buf[0:4], uint32(len(head)))
packet = append(packet, buf...) //写包头长度
if len(head) > 0 {
packet = append(packet, head...) //写包头
}
//写数据
//取数据长
binary.BigEndian.PutUint32(buf[0:4], uint32(len(data)))
packet = append(packet, buf...) //写数据长度
if len(data) > 0 {
packet = append(packet, data...) //写数据
}
if len(packet) != cap(packet) {
return false
} //数据异常
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
session.writeCh <- packet
return <-session.writeResultCh
}
//读取数据
func (session *Session) readDataWithLength() []byte {
var dataLen uint32 //数据长度
var lenBuffer [4]byte //数据长度缓存区
var data []byte //数据缓存区
//获取数据长度
readedLen, err := session.conn.Read(lenBuffer[0:4])
if err != nil || readedLen != 4 {
PrintError("获取消息失败。 Error: " + err.Error())
return nil
}
dataLen = binary.BigEndian.Uint32(lenBuffer[0:4])
data = make([]byte, dataLen)
if dataLen == 0 {
return data
}
readedLen, err = session.conn.Read(data)
if err != nil || readedLen != int(dataLen) {
PrintError("获取消息失败。Error" + err.Error())
return nil
}
return data
}
//发送数据
func (session *Session) SendData(headInfo map[string]string, data []byte) bool {
head, err := json.Marshal(headInfo)
if err != nil {
PrintError("Marshal Failed.")
return false
}
return session.send(head, data)
}
//发送消息
func (session *Session) SendMessage(msg []byte) bool {
if session.sender == nil {
PrintError("请先登录")
return false
}
if session.receiver == "" {
PrintError("请先设定当前聊天好友")
return false
}
if len(msg) == 0 {
PrintError("无法发送空消息")
return false
}
PrintMessageHead(session.sender.NickName+" ["+time.Now().Format("2006-01-02 15:04:05")+"]", true)
PrintMessage(string(msg))
headInfo := MakeHead(operation_send_message)
headInfo["To"] = session.receiver
return session.SendData(headInfo, msg)
}
//登录
func (session *Session) Login(user string, pwd string) bool {
logInfo := MakeHead(operation_login)
logInfo["User"] = user
logInfo["Pwd"] = pwd
return session.SendData(logInfo, nil)
}
//请求登出
func (session *Session) Logout() {
if session.sender == nil {
PrintError("当前未登录")
return
} //未登录
headInfo := MakeHead(operation_logout)
session.SendData(headInfo, nil)
atomic.SwapInt32(&session.status, session_status_created)
session.sender = nil
session.friends = nil
session.receiver = ""
SetUserNameText("未登录")
SetCurrentFriendText("")
PrintLog("已登出.")
}
//获取好友列表
func (session *Session) GetUserList() {
headInfo := MakeHead(operation_get_user_list)
session.SendData(headInfo, nil)
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
package main
//用户信息
type UserInfo struct {
User string //用户名
NickName string //昵称
Motto string //签名
UGroup string //用户组。ugroup_*常量值
}
+158
View File
@@ -0,0 +1,158 @@
package main
import (
"math"
"strings"
"syscall"
"unsafe"
)
const (
FOREGROUND_BLUE int = 1
FOREGROUND_GREEN int = 2
FOREGROUND_RED int = 4
FOREGROUND_INTENSITY int = 8
BACKGROUND_BLUE int = 16
BACKGROUND_GREEN int = 32
BACKGROUND_RED int = 64
BACKGROUND_INTENSITY int = 128
FORGROUND_WHITE int = 7
)
var conctrl *syscall.LazyDLL //Windows控制台窗口辅助输出库
var consoleWindow uintptr //控制台窗口控制器指针
var messagePannel uintptr //消息输出窗格
var operationPannel uintptr //操作提示窗格
var usrenamePannel uintptr //用户名显示窗格
var currentFriendPannel uintptr //当前聊天对象显示窗格
var inputPannel uintptr //输入窗格
var spliter uintptr //分割线
//初始化
func InitConsole() {
conctrl = syscall.NewLazyDLL("conctrl_x64.dll")
proc := conctrl.NewProc("CreateConsoleWindow")
consoleWindow, _, _ = proc.Call(100, 999)
if consoleWindow == 0 {
panic("Failed to init console. conctrl.dll not found.")
}
proc = conctrl.NewProc("CreatePannel")
messagePannel, _, _ = proc.Call(consoleWindow, 0, 0, 100, 20)
operationPannel, _, _ = proc.Call(consoleWindow, 0, 21, 20, 1)
usrenamePannel, _, _ = proc.Call(consoleWindow, 35, 21, 30, 1)
currentFriendPannel, _, _ = proc.Call(consoleWindow, 65, 21, 35, 1)
inputPannel, _, _ = proc.Call(consoleWindow, 0, 23, 100, 20)
proc = conctrl.NewProc("CreateSpliter")
spliter, _, _ = proc.Call(consoleWindow, 0, 22, 100, 0, uintptr(FORGROUND_WHITE))
proc = conctrl.NewProc("FocusOnPannel")
proc.Call(inputPannel, 0, 0)
SetTitle("StormChat")
}
//滚动输出区域
func ScrollOutputArea(lineCount int) {
var proc *syscall.LazyProc
if lineCount > 0 {
proc = conctrl.NewProc("ScrollPannelForward")
} else {
proc = conctrl.NewProc("ScrollPannelBackward")
}
proc.Call(messagePannel, uintptr(math.Abs(float64(lineCount))))
}
//设置控制台标题
func SetTitle(title string) {
proc := conctrl.NewProc("SetConsoleWindowTitle")
titleB := append([]byte(title), 0)
pTtile := *(*uintptr)(unsafe.Pointer(&titleB))
proc.Call(uintptr(pTtile), 1)
}
//设置当前操作提示文本
func SetOperationText(op string) {
proc := conctrl.NewProc("ClearPannel")
proc.Call(operationPannel)
AddPannelLine(operationPannel, op, false, FORGROUND_WHITE)
}
//设置用户昵称显示区文本
func SetUserNameText(name string) {
blank := (30 - len(name)) / 2
if blank > 0 {
name = strings.Repeat(" ", blank) + name
}
proc := conctrl.NewProc("ClearPannel")
proc.Call(usrenamePannel)
AddPannelLine(usrenamePannel, name, false, FORGROUND_WHITE)
}
//设置当前聊天好友提示文本
func SetCurrentFriendText(friend string) {
if friend != "" {
friend = "To: " + friend
}
blank := 34 - len(friend)
if blank > 0 {
friend = strings.Repeat(" ", blank) + friend
}
proc := conctrl.NewProc("ClearPannel")
proc.Call(currentFriendPannel)
AddPannelLine(currentFriendPannel, friend, false, FORGROUND_WHITE)
}
//向输出区写行
func PrintOutputLine(text string, attribute int) {
AddPannelLine(messagePannel, text, false, attribute)
}
//向输入区写文本
func PrintInputTip(text string) {
ClearInput()
AddPannelText(inputPannel, text, true, FORGROUND_WHITE)
}
//清空输入区
func ClearInput() {
proc := conctrl.NewProc("ClearPannel")
proc.Call(inputPannel)
proc = conctrl.NewProc("FocusOnPannel")
proc.Call(inputPannel, 0, 0)
}
func ClearOutput() {
proc := conctrl.NewProc("ClearPannel")
proc.Call(messagePannel)
}
//销毁控制器,恢复控制台
func DestroyConsoleWindow() {
proc := conctrl.NewProc("FocusOnPannel")
proc.Call(messagePannel, 0, 0)
proc = conctrl.NewProc("DestroyConsoleWindow")
proc.Call(consoleWindow)
}
//向窗格加入文本行
func AddPannelLine(pannel uintptr, text string, focus bool, attribute int) {
proc := conctrl.NewProc("AddPannelLine")
addPannel(proc, pannel, text, focus, attribute)
}
//向窗格加入文本
func AddPannelText(pannel uintptr, text string, focus bool, attribute int) {
proc := conctrl.NewProc("AddPannelText")
addPannel(proc, pannel, text, focus, attribute)
}
//向窗格加入文本(行)
func addPannel(proc *syscall.LazyProc, pannel uintptr, text string, focus bool, attribute int) {
line := append([]byte(text), 0)
pLine := *(*uintptr)(unsafe.Pointer(&line))
var focusInt int
if focus {
focusInt = 1
} else {
focusInt = 0
}
proc.Call(pannel, uintptr(pLine), uintptr(focusInt), 1, uintptr(attribute))
}
+28
View File
@@ -0,0 +1,28 @@
DROP TABLE IF EXISTS `message`;
CREATE TABLE `message` (
`Id` int(11) NOT NULL AUTO_INCREMENT,
`When` varchar(44) NOT NULL,
`From` varchar(12) NOT NULL,
`To` varchar(12) NOT NULL,
`Msg` text,
PRIMARY KEY (`Id`)
) AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
LOCK TABLES `message` WRITE;
UNLOCK TABLES;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`User` varchar(12) NOT NULL,
`Pwd` varchar(16) NOT NULL,
`NickName` varchar(36) NOT NULL DEFAULT '',
`Motto` varchar(144) NOT NULL DEFAULT '',
`UGroup` enum('User','Vip','Admin','Group') DEFAULT 'User',
`Photo` mediumtext,
PRIMARY KEY (`User`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
LOCK TABLES `user` WRITE;
INSERT INTO `user` VALUES ('mattuy','mattuy','Mattuy','','Admin',NULL);
UNLOCK TABLES;
+72
View File
@@ -0,0 +1,72 @@
package main
//宏
const (
//是否为服务模式,调试时置为false以输出调试信息
serveMode = false
//最大包头长度
max_head_length uint32 = 4096 //4K
//最大消息长度
max_message_length uint32 = 0x6400000 //64M
//最大头像大小
max_photo_size uint32 = 0x400000 //4M
//最大数据等待时间,单位秒。客户端应发送心跳包以保持连接。调试,先设为100年
timeout_message = 100 * 365 * 24 * 60 * 60
)
//用户组
const (
ugroup_user string = "User" //普通用户
ugroup_vip string = "Vip" //会员
ugroup_admin string = "Admin" //管理员
ugroup_group string = "Group" //群聊
)
//消息头的通用字段名称
const (
keyname_operation string = "Operation" //操作名
keyname_token string = "Token"
)
//控制消息
const (
operation_panic string = "Panic"
operation_trans_message string = "TransMessage"
operation_send_message string = "SendMessage"
operation_ping string = "Ping"
operation_login string = "Login"
operation_logout string = "Logout"
operation_offline string = "Offline"
operation_get_user_list string = "GetUserList"
operation_update_userinfo string = "UpdateUserInfo"
)
//字符串常量
const (
//日志文件。交互模式时无效(输出到os.Stdout)
str_log_file string = "/var/log/stormchat.log"
//mysql连接字符串
str_db_conn_str string = "stormchat:stormchat@tcp(localhost:3306)/stormchat?charset=utf8"
//登陆
str_sql_login string = "SELECT `User`, `NickName`, `Motto`, `UGroup` FROM `user` WHERE `User`=? AND `Pwd`=? LIMIT 1"
//获取用户信息
str_sql_query_user string = "SELECT `User`, `NickName`, `Motto`, `UGroup` FROM `user` WHERE `User`=? LIMIT 1"
//获取头像
str_sql_get_photo string = "SELECT `Photo` FROM `user` WHERE `User`=?"
//设置昵称
str_sql_update_nickname string = "UPDATE `user` SET `NickName`=? WHERE `User`=?"
//设置密码
str_sql_update_password string = "UPDATE `user` SET `Pwd`=? WHERE `User`=?"
//设置签名
str_sql_update_motto string = "UPDATE `user` SET `Motto`=? WHERE `User`=?"
//设置头像
str_sql_update_photo string = "UPDATE `user` SET `Photo`=? WHERE `User`=?"
//获取用户列表(有序)
str_sql_get_users string = "SELECT `User`, `NickName`, `Motto`, `UGroup` FROM `user` ORDER BY `User`"
//SQL语句-消息入库
str_sql_save_message string = "INSERT INTO `message` (`When`, `From`, `To`, `Msg`) VALUES(?, ?, ?, ?)"
//SQL语句-消息出库
str_sql_get_message string = "SELECT `Id`, `When`, `From`, `To`, `Msg` FROM `message` WHERE `To`=? ORDER BY `When`"
//SQL语句-消息已送出,清除
str_sql_delete_message string = "DELETE FROM `message` WHERE `Id`=?"
)
+19
View File
@@ -0,0 +1,19 @@
package main
import (
"fmt"
"time"
)
//写日志
func WriteLog(err string) {
fmt.Fprintln(server.logFile, time.Now().String(), " ", err)
}
//输出调试信息
func Debug(msg string) {
if serveMode {
return
}
fmt.Println(msg)
}
+46
View File
@@ -0,0 +1,46 @@
//主程序
package main
import (
"bufio"
"fmt"
"net"
"os"
)
var server = NewStormServer()
func main() {
if server == nil {
fmt.Println("Failed to create service.")
return
}
tcpAddr, _ := net.ResolveTCPAddr("tcp", ":3727")
tcpListener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("Failed to start listening.")
return
}
if serveMode {
server.AcceptLoop(tcpListener)
return
} //服务模式
//交互模式
go server.AcceptLoop(tcpListener)
for {
inputReader := bufio.NewReader(os.Stdin)
cmd, _ := inputReader.ReadString('\n')
switch cmd {
case "help\n":
fmt.Println("help")
break
case "quit\n":
case "exit\n":
fmt.Println("bye")
break
default:
fmt.Println("invalid command.")
break
}
}
}
+90
View File
@@ -0,0 +1,90 @@
package main
import (
"encoding/base64"
"time"
)
//消息结构
type Message struct {
When time.Time //接收时间
From *UserInfo //发送者
To *UserInfo //接收者
Msg []byte //消息体
}
//消息存储结构,用于消息数据入库/出库
type MessageInfo struct {
Id int
When string
From string
To string
Msg string
}
func NewMessage(msgInfo *MessageInfo) *Message {
m := new(Message)
m.When, _ = time.Parse(time.RFC3339, msgInfo.When)
m.From = QueryUserInfo(msgInfo.From)
m.To = QueryUserInfo(msgInfo.To)
var err error
m.Msg, err = base64.StdEncoding.DecodeString(msgInfo.Msg)
if m.From == nil || m.To == nil || err != nil {
return nil
}
return m
}
//将消息存储到数据库
func (message *Message) Save() bool {
_, err := server.db.Exec(str_sql_save_message, message.When.Format(time.RFC3339),
message.From.User, message.To.User, base64.StdEncoding.EncodeToString(message.Msg))
if err != nil {
WriteLog("SaveMessage Error: " + err.Error())
return false
}
return true
}
//查询用户未读消息
func QuerySavedMessages(user *UserInfo) map[int]Message {
messageMap := make(map[int]Message)
rows, err := server.db.Query(str_sql_get_message, user.User)
if err != nil {
WriteLog("[QuerySavedMessage]Quering Failed: " + err.Error())
return nil
}
for rows.Next() {
msgInfo := new(MessageInfo)
scanErr := rows.Scan(&msgInfo.Id, &msgInfo.When, &msgInfo.From, &msgInfo.To, &msgInfo.Msg)
if scanErr != nil {
WriteLog("[QuerySavedMessage]Scan Failed: " + scanErr.Error())
rows.Close()
return nil
}
m := NewMessage(msgInfo)
if m == nil {
WriteLog("[QuerySavedMessage]Bad Message. " + err.Error())
DeleteSavedMessage(msgInfo.Id) //删除无效消息
} else {
messageMap[msgInfo.Id] = *m
}
}
err = rows.Err()
if err != nil {
WriteLog("[QuerySavedMessage]Rows Failed: " + err.Error())
return nil
}
return messageMap
}
//删除数据库中的未读消息
func DeleteSavedMessage(id int) bool {
_, err := server.db.Exec(str_sql_delete_message, id)
if err != nil {
WriteLog("[DeleteSavedMessage]Delete Message Failed: " + err.Error())
return false
}
return true
}
+451
View File
@@ -0,0 +1,451 @@
package main
import (
"encoding/binary"
"encoding/json"
"net"
"strconv"
"strings"
"sync/atomic"
"time"
)
//当前session的状态
const (
session_status_created = iota //已创建,但未确认发送者和接收者身份
session_status_running // 通道已建立,session正常运行
session_status_stoped // 标识服务已停止
session_status_destroyed //消息读写线程已关闭,连接已断开
)
type Session struct {
sender *UserInfo //消息发送者
receiver string //消息接收者
addr string //客户端地址
conn *net.TCPConn //TCP连接
status int32 //session运行状态
stopChan chan bool //Session终止标识,传递数据即终止。仅由读消息循环用于终止写循环
writeCh chan []byte //写消息通道
writeResultCh chan bool //是否写成功
}
//初始化
func NewSession(conn *net.TCPConn) *Session {
var session = new(Session)
session.sender = nil
session.receiver = ""
session.status = session_status_created
session.stopChan = make(chan bool)
session.writeCh = make(chan []byte)
session.writeResultCh = make(chan bool)
session.conn = conn
session.addr = conn.RemoteAddr().String()
return session
}
//销毁聊天连接
func (session *Session) Destroy() {
if session.status == session_status_destroyed {
return
}
if session.sender != nil {
delete(server.sessionMap, session.sender.User)
}
session.status = session_status_destroyed
session.conn.Close()
close(session.writeCh)
close(session.writeResultCh)
}
//消息处理总控制器,根据控制字调用相应处理函数
//关闭连接时先退出读循环,再关闭写循环,避免造成chan panic
func (session *Session) ReceiveLoop() {
defer func() {
session.stopChan <- true
Debug("[" + session.addr + "]Receive loop ended.")
}() //终止写循环
Debug("[" + session.addr + "]Receive loop started.")
var err error //错误
for {
if atomic.LoadInt32(&session.status) == session_status_stoped {
return
} //检查session是否已终止
//设置超时并等待客户数据
if !session.waitData(time.Now().Add(timeout_message * time.Second)) {
continue
}
//读取数据
head := session.readDataWithLength(max_head_length)
data := session.readDataWithLength(max_message_length)
if head == nil || data == nil {
Debug("Failed to read data.")
session.SendPanic("", "Failed to read data.")
continue
}
//解析包头
headInfo := make(map[string]string)
err = json.Unmarshal(head, &headInfo)
if err != nil {
Debug("Failed to unmarshal head.")
session.SendPanic("", "Failed to unmarshal head.")
continue
}
if session.sender == nil && headInfo[keyname_operation] != operation_login {
session.SendPanic("", "MUST LOGIN")
continue
} //抛弃未登录用户的消息
switch headInfo[keyname_operation] {
case operation_send_message:
go session.MessageHandler(headInfo, data)
break
case operation_ping:
go session.PingHandler()
break
case operation_login:
session.LoginHandler(headInfo)
break
case operation_logout:
session.LogoutHandler()
break
case operation_get_user_list:
go session.GetUserListHandler(headInfo)
break
case operation_update_userinfo:
go session.UpdateUserInfoHandler(headInfo, data)
break
default:
session.SendPanic(headInfo[keyname_operation], "Unknwon Request")
continue
}
}
}
//消息写循环(由读循环终止)
func (session *Session) SendLoop() {
Debug("[" + session.addr + "]Send loop started.")
defer session.Destroy()
for {
select {
case data, ok := <-session.writeCh:
if !ok {
atomic.SwapInt32(&session.status, session_status_stoped)
continue
}
writeLen, err := session.conn.Write(data)
if err != nil || (writeLen != len(data)) {
WriteLog(err.Error())
atomic.SwapInt32(&session.status, session_status_stoped)
session.writeResultCh <- false
continue
} //写失败
session.writeResultCh <- true
case <-session.stopChan:
Debug("[" + session.addr + "]Send loop ended.")
return
}
}
}
//响应客户端心跳包
func (session *Session) PingHandler() {
return
}
//登录事件处理函数
func (session *Session) LoginHandler(headInfo map[string]string) {
returnInfo := make(map[string]string)
returnInfo[keyname_operation] = headInfo[keyname_operation]
returnInfo[keyname_token] = headInfo[keyname_token]
returnInfo["Error"] = ""
if session.status != session_status_created {
session.SendData(returnInfo, nil)
return
} //已登录
user := Login(headInfo["User"], headInfo["Pwd"])
if user == nil {
Debug("Bad login: illegal user.")
returnInfo["Error"] = "Illegal User."
session.SendData(returnInfo, nil)
return
}
session.sender = user
session.status = session_status_running
session.Offline(server.sessionMap[session.sender.User], "The account is logged by another client.")
server.sessionMap[session.sender.User] = session
userdata, _ := json.Marshal(user)
session.SendData(returnInfo, userdata)
//转发用户未读消息
go session.TransUnreadedMessages(session.sender)
Debug("Log in successfully.")
}
//用户登出
func (session *Session) LogoutHandler() {
//防止其他session传递数据。等待用户断开连接或者再次登录
delete(server.sessionMap, session.sender.User)
session.sender = nil
session.receiver = ""
if atomic.LoadInt32(&session.status) == session_status_running {
atomic.SwapInt32(&session.status, session_status_created)
} //恢复未登录状态
}
//消息处理函数
func (session *Session) MessageHandler(headInfo map[string]string, msg []byte) {
result := make(map[string]string)
result[keyname_token] = headInfo[keyname_token]
result[keyname_operation] = operation_send_message
result["Error"] = ""
if headInfo["To"] == "" {
headInfo["To"] = session.receiver
}
var destChat *Session = nil
for key, value := range server.sessionMap {
if key == headInfo["To"] {
destChat = value
break
}
} //判断接收者是否在线
message := new(Message)
message.When = time.Now()
message.From = session.sender
message.Msg = msg
if destChat != nil {
message.To = destChat.sender
destChat.TransMessage(message)
} else {
message.To = QueryUserInfo(headInfo["To"])
if message.To != nil {
message.Save()
} else {
result["Error"] = "The message doesn't has a valid receiver."
} //消息接收者不存在
}
if _, ok := headInfo["NeedResult"]; ok {
session.SendData(result, nil)
} //返回执行结果
}
//获取用户列表
func (session *Session) GetUserListHandler(headInfo map[string]string) bool {
head := make(map[string]string)
head[keyname_token] = headInfo[keyname_token]
head[keyname_operation] = headInfo[keyname_operation]
head["Count"] = "0"
head["Error"] = ""
users := GetUserList()
if users == nil {
head["Error"] = "Server failed to get user list."
session.SendData(head, nil)
return false
}
data, err := json.Marshal(users)
if err != nil {
head["Error"] = "JSON marshal failed: " + err.Error()
WriteLog(head["Error"])
session.SendData(head, nil)
return false
}
head["Count"] = string(len(users))
return session.SendData(head, data)
}
//获取用户头像
func (session *Session) GetUserPhotoHandler(headInfo map[string]string) {
head := make(map[string]string)
head[keyname_token] = headInfo[keyname_token]
head[keyname_operation] = headInfo[keyname_operation]
head["Error"] = ""
photo := GetUserPhoto(session.sender.User)
session.SendData(head, photo)
}
//更改用户信息
func (session *Session) UpdateUserInfoHandler(headInfo map[string]string, data []byte) {
//准备反馈数据
result := make(map[string]string)
result[keyname_token] = headInfo[keyname_token]
result[keyname_operation] = headInfo[operation_update_userinfo]
result["Error"] = ""
if item, ok := headInfo["NickName"]; ok {
result["Error"] += session.sender.UpdateNickName(item) + ";"
} //修改昵称
if item, ok := headInfo["Password"]; ok {
result["Error"] += session.sender.UpdatePassword(item) + ";"
} //修改密码
if item, ok := headInfo["Motto"]; ok {
result["Error"] += session.sender.UpdateMotto(item) + ";"
} //修改签名
item, ok := headInfo["Photo"]
i, _ := strconv.Atoi(item)
if ok && i > 0 {
result["Error"] += session.sender.UpdatePhoto(data)
} //修改头像
session.SendData(result, nil)
}
/**
* 服务器数据异常向客户端发送Panic并关闭连接
* @param curOperation, 当前操作
* @param errStr, 错误描述
*/
func (session *Session) SendPanic(curOperation string, errStr string) bool {
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
Debug("[SendPanic]" + errStr)
defer atomic.SwapInt32(&session.status, session_status_stoped)
data := make(map[string]string)
data[keyname_token] = ""
data[keyname_operation] = operation_panic
data["Job"] = curOperation
data["Error"] = errStr
slice, err := json.Marshal(data)
if err != nil {
return false
}
return session.send(slice, nil)
}
//发送数据到客户端,包头将被json序列化
func (session *Session) SendData(headInfo map[string]string, data []byte) bool {
head, err := json.Marshal(headInfo)
if err != nil {
Debug("TransMessage Failed.")
session.SendPanic(headInfo[keyname_operation], "Failed to marshal data.")
return false
}
return session.send(head, data)
}
//勒令用户下线。调用条件(满足一条):
//相同账户被重复登录
//用户修改密码
func (newChat *Session) Offline(oldChat *Session, reason string) bool {
if oldChat == nil {
return true
}
data := make(map[string]string)
data[keyname_token] = ""
data[keyname_operation] = operation_offline
data["Error"] = "Another Login"
data["Addr"] = newChat.addr
defer atomic.SwapInt32(&oldChat.status, session_status_stoped)
return oldChat.SendData(data, nil)
}
//转发消息
func (session *Session) TransMessage(msg *Message) bool {
data := make(map[string]string)
data[keyname_token] = ""
data[keyname_operation] = operation_trans_message
data["When"] = msg.When.Format(time.RFC3339)
data["From"] = msg.From.User
return session.SendData(data, []byte(msg.Msg))
}
//转发存储在数据库中的未读消息
func (session *Session) TransUnreadedMessages(user *UserInfo) bool {
messages := QuerySavedMessages(user)
if messages == nil {
return false
}
for id, message := range messages {
if session.TransMessage(&message) {
DeleteSavedMessage(id)
} else {
return false
}
}
Debug("Unreaded message transmitting done.")
return true
}
//从TCP连接读取数据,取到的前2字节作为数据长度
func (session *Session) readDataWithLength(max_length uint32) []byte {
var dataLen uint32 //数据长度
var lenBuffer [4]byte //数据长度缓存区
var data []byte //数据缓存区
//获取数据长度
readedLen, err := session.conn.Read(lenBuffer[0:4])
if err != nil || readedLen != 4 {
Debug("Failed to read data. Error: " + err.Error())
return nil
}
dataLen = binary.BigEndian.Uint32(lenBuffer[0:4])
if dataLen > max_length || dataLen < 0 {
WriteLog("[ReadData]Data length exceeds.")
return nil
} //数据长度超出限制
data = make([]byte, dataLen)
if dataLen == 0 {
return data
}
readedLen, err = session.conn.Read(data)
if err != nil || readedLen != int(dataLen) {
Debug("[ReadData]Failed to read data." + err.Error())
return nil
}
return data
}
//等待客户端数据
func (session *Session) waitData(deadline time.Time) bool {
var buf = make([]byte, 0, 0) //空缓冲区
for time.Now().Before(deadline) {
session.conn.SetReadDeadline(time.Now().Add(time.Second))
_, err := session.conn.Read(buf)
if err == nil {
//消息到达时临时禁止读取超时
session.conn.SetReadDeadline(time.Now().Add(time.Hour * 24 * 360 * 100))
return true
} //数据到达
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
if strings.HasSuffix(err.Error(), "i/o timeout") {
continue
} //等待数据时正常超时
atomic.SwapInt32(&session.status, session_status_stoped)
return false
}
return false
}
/**
* 将数据加工后发送给客户端
* @param head, 消息头
* @param data, 消息体
*/
func (session *Session) send(head []byte, data []byte) bool {
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
var buf = make([]byte, 4) //长度标识
var packet = make([]byte, 0, len(head)+len(data)+4+4) //headLen + head + msgLen + data
//写包头
//取包头长
binary.BigEndian.PutUint32(buf[0:4], uint32(len(head)))
packet = append(packet, buf...) //写包头长度
if len(head) > 0 {
packet = append(packet, head...) //写包头
}
//写数据
//取数据长
binary.BigEndian.PutUint32(buf[0:4], uint32(len(data)))
packet = append(packet, buf...) //写数据长度
if len(data) > 0 {
packet = append(packet, data...) //写数据
}
if len(packet) != cap(packet) {
return false
} //数据异常
if atomic.LoadInt32(&session.status) >= session_status_stoped {
return false
}
session.writeCh <- packet
return <-session.writeResultCh
}
@@ -0,0 +1,59 @@
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"net"
"os"
)
type StormServer struct {
listener *net.TCPListener
logFile *os.File //日志文件
db *sql.DB
sessionMap map[string]*Session //session列表
}
//创建服务
func NewStormServer() *StormServer {
var newServer = new(StormServer)
var err error
//打开日志文件
if serveMode {
newServer.logFile, err = os.OpenFile(str_log_file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 002)
if err != nil {
Debug("Failed to open log file.")
return nil
}
} else {
newServer.logFile = os.Stderr
}
//连接数据库
newServer.db, _ = sql.Open("mysql", str_db_conn_str)
err = newServer.db.Ping()
if err != nil {
Debug("Failed to connect to the database.")
return nil
}
newServer.sessionMap = make(map[string]*Session)
return newServer
}
//销毁服务
func (server *StormServer) Destroy() {
if !serveMode {
server.logFile.Close()
}
server.db.Close()
}
//监听新的TCP连接
func (server *StormServer) AcceptLoop(tcpListener *net.TCPListener) {
for {
tcpConn, _ := tcpListener.AcceptTCP()
var newChat = NewSession(tcpConn)
go newChat.SendLoop()
go newChat.ReceiveLoop()
}
}
+133
View File
@@ -0,0 +1,133 @@
package main
import (
"encoding/base64"
"unicode/utf8"
)
//用户信息
type UserInfo struct {
User string //用户名
Nickname string //昵称
Motto string //签名
UGroup string //用户组。ugroup_*常量值
}
//根据用户名查询一个用户。如果用户不存在则返回nil
func QueryUserInfo(userName string) *UserInfo {
user := new(UserInfo)
err := server.db.QueryRow(str_sql_query_user, userName).Scan(&user.User, &user.Nickname, &user.Motto, &user.UGroup)
if err != nil {
return nil
} else {
return user
}
}
//获取用户头像
func GetUserPhoto(userName string) []byte {
var photo string
err := server.db.QueryRow(str_sql_get_photo, userName).Scan(&photo)
if err != nil {
return nil
}
photoData, err := base64.StdEncoding.DecodeString(photo)
if err != nil {
return nil
}
return photoData
}
//登录,成功返回用户信息,失败返回nil
func Login(userName string, pwd string) *UserInfo {
user := new(UserInfo)
err := server.db.QueryRow(str_sql_login, userName, pwd).Scan(&user.User, &user.Nickname, &user.Motto, &user.UGroup)
if err != nil {
if err.Error() == "sql: no rows in result set" {
Debug(err.Error())
} else {
WriteLog("[Login]Database Error: " + err.Error())
}
return nil
} else {
return user
}
}
//获取用户列表
func GetUserList() []UserInfo {
users := make([]UserInfo, 0, 10)
rows, err := server.db.Query(str_sql_get_users)
defer rows.Close()
if err != nil {
WriteLog("Database Error: " + err.Error())
return nil
}
for rows.Next() {
user := new(UserInfo)
scanErr := rows.Scan(&user.User, &user.Nickname, &user.Motto, &user.UGroup)
if scanErr != nil {
WriteLog("Database Error - Scan Error: " + scanErr.Error())
return nil
}
users = append(users, *user)
}
err = rows.Err()
if err != nil {
WriteLog("Database Error: " + err.Error())
return nil
}
return users
}
//更改昵称
func (user *UserInfo) UpdateNickName(newNickName string) (errText string) {
if length := utf8.RuneCountInString(newNickName); length == 0 || length > 12 {
return "Illeagal NickName"
}
_, err := server.db.Exec(str_sql_update_nickname, newNickName, user.User)
if err != nil {
WriteLog("[UpdateNickName]DB Error: " + err.Error())
return "Database Error"
}
return ""
}
//更新密码
func (user *UserInfo) UpdatePassword(newPassword string) (errText string) {
if length := len([]rune(newPassword)); length == 0 || length > 16 {
return "Illeagal Password"
}
_, err := server.db.Exec(str_sql_update_password, newPassword, user.User)
if err != nil {
WriteLog("[UpdatePassword]DB Error: " + err.Error())
return "Database Error"
}
return ""
}
//更新签名
func (user *UserInfo) UpdateMotto(newMotto string) (errText string) {
if length := utf8.RuneCountInString(newMotto); length > 32 {
return "Motto is too long"
}
_, err := server.db.Exec(str_sql_update_motto, newMotto, user.User)
if err != nil {
WriteLog("[UpdateMotto]DB Error: " + err.Error())
return "Database Error"
}
return ""
}
//更新头像
func (user *UserInfo) UpdatePhoto(data []byte) (errText string) {
if uint32(len(data)) > max_photo_size {
return "New photo is too large"
}
_, err := server.db.Exec(str_sql_update_photo, base64.StdEncoding.EncodeToString(data), user.User)
if err != nil {
WriteLog("[UpdatePhoto]DB Error: " + err.Error())
return "Database Error"
}
return ""
}