Compare commits

46 Commits

Author SHA1 Message Date
Kaniu Tayou 200d153590 更改 2019-01-15 18:19:46 +08:00
Kaniu Tayou 82b3f06a9d UI设计 2019-01-15 18:17:28 +08:00
Kaniu Tayou 97dd873921 日常察看 2019-01-04 11:34:19 +08:00
Kaniu Tayou 6cea1b61c7 完成使用者信息显示 2018-12-29 17:32:32 +08:00
Kaniu Tayou b42c7a132f 用户自身头像显示 未完成 2018-12-29 17:04:23 +08:00
Kaniu Tayou b95581b63c 优化布局 2018-12-29 08:28:31 +08:00
Kaniu Tayou 688a53a9dd Listview完整显示用户头像及昵称信息 2018-12-27 17:42:26 +08:00
Kaniu Tayou 96c128b295 用户头像显示 2018-12-25 22:45:58 +08:00
Mattuy Lee 8ec162c5df Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat.git 2018-12-24 17:55:21 +08:00
Mattuy Lee 612a5b8645 修改用户头像数据类型 2018-12-24 17:54:50 +08:00
KaniuTayou fae7ce37e5 未完成,保存 2018-12-24 17:53:55 +08:00
Mattuy f28ad69067 merge到master 2018-12-23 15:09:24 +08:00
Mattuy 0810ee4407 commonly commit 2018-12-23 15:02:53 +08:00
Kaniu Tayou 331e93aeeb 优化显示,一些异常的处理 2018-12-20 20:33:15 +08:00
Kaniu Tayou d51784bbc3 chatbubble封装 2018-12-20 20:06:10 +08:00
Mattuy cd90e6bd30 Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat into client-csharp 2018-12-20 16:53:16 +08:00
Mattuy 02c67c9b29 编写后台数据交互API 2018-12-20 16:52:29 +08:00
Kaniu Tayou bc941e5c5c 滚动条 2018-12-20 00:18:49 +08:00
Kaniu Tayou 1ea4296423 前端实现接收发送文本消息 2018-12-19 23:53:46 +08:00
Kaniu Tayou 72b62b5cb6 合并 2018-12-18 23:56:45 +08:00
Kaniu Tayou ecf4d7908e 完善登录窗口UI设计 2018-12-18 23:55:34 +08:00
Mattuy 4d28316f67 完善发送消息和接收消息接口 2018-12-18 23:35:11 +08:00
Mattuy 308b9494d7 commonly commit 2018-12-18 21:03:14 +08:00
Mattuy d4d9ebf180 合并 2018-12-18 20:27:22 +08:00
Mattuy 8361b01894 commonly commit 2018-12-18 20:24:39 +08:00
Kaniu Tayou 93880aba81 合并项目 2018-12-18 19:40:39 +08:00
Kaniu Tayou ad745595e8 一些新设定 2018-12-18 19:38:27 +08:00
Kaniu Tayou 30f41f5220 一些改动 2018-12-18 11:35:23 +08:00
Kaniu Tayou d08d864fda some new methods 2018-12-18 11:17:57 +08:00
Kaniu Tayou 29b281a71f Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat into client-csharp 2018-12-17 22:00:40 +08:00
Mattuy 08dd282350 Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat into client-csharp 2018-12-17 21:54:46 +08:00
Mattuy 82bc190cf9 修改获取用户列表API 2018-12-17 21:53:52 +08:00
Kaniu Tayou f296fa5570 同步代码数据 2018-12-17 18:51:57 +08:00
Kaniu Tayou 8728101cb2 新wpf窗口建立 2018-12-17 18:49:16 +08:00
Kaniu Tayou b918b132a7 一些客户端方法框架的搭建 2018-12-17 17:17:44 +08:00
Mattuy 9894de2953 Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat into client-csharp 2018-12-17 11:28:19 +08:00
Mattuy 69a61ef446 编写向服务器请求数据的相关函数 2018-12-17 11:27:23 +08:00
Kaniu Tayou e97b49667e WPF userwindow and logwindow 2018-12-15 22:51:48 +08:00
Kaniu Tayou 55a82b5a4f Merge branch 'client-csharp' of https://github.com/mattuylee/stormchat into client-csharp 2018-12-15 21:12:09 +08:00
Kaniu Tayou 2177952236 new wpf window 2018-12-15 21:11:56 +08:00
Mattuy 5788d698eb commenly commit 2018-12-15 21:10:10 +08:00
Mattuy a953aca9f1 commonly commit 2018-12-15 20:45:17 +08:00
Mattuy 0e6d1922e0 完善事件框架 2018-12-15 17:54:19 +08:00
Mattuy 145c242d4b initial commit for client with c# 2018-12-15 00:29:41 +08:00
Mattuy Lee a6190a9bb1 update .ignore file
for both golang and visual c#
2018-12-15 00:23:23 +08:00
Mattuy Lee 1b26347812 Initial commit 2018-12-15 00:17:57 +08:00
45 changed files with 1771 additions and 3728 deletions
-285
View File
@@ -1,285 +0,0 @@
# StormChat
## 目录
* <a href="#intro">简介</a>
* <a href="#design">设计思路</a>
* <a href="#design-comunication">通信流程</a>
* <a href="#design-packet-struct">通信数据包结构</a>
* <a href="#env-config">环境配置</a>
* <a href="#env-config-dev">开发环境</a>
* <a href="#env-config-run">运行环境</a>
* <a href="#soft-config">程序配置</a>
* <a href="#soft-config-server">服务器端</a>
* <a href="#soft-config-client-go">客户端(Go</a>
* <a href="#soft-config-client-cs">客户端(C#</a>
* <a href="#build">如何编译</a>
* <a href="#build-server">服务器端</a>
* <a href="#build-client-go">客户端(Go</a>
* <a href="#build-client-cs">客户端(C#</a>
* <a href="#warning">注意事项</a>
* <a href="#dir-struct">目录结构</a>
* <a href="#usage-pic">使用截图</a>
<a name="intro"></a>
## 简介
&emsp;&emsp;前段时间心血来潮想学习最近的明星编程语言Golang。于是想做个聊天小程序实践一下。程序基于TCP协议通信,更详细的设计见<a href="#design">设计思路</a>。
&emsp;&emsp;用Golang实现了服务端程序,同时代码抄抄改改做了个Golang客户端,又由于输出问题,用C++写了个Windows控制台的简陋的[输出控制库](https://github.com/mattuylee/conctrl),于是客户端在Windows上能看了(不过朋友说很丑>_<)。
&emsp;&emsp;在朋友([@Billows](https://github.com/KaniuBillows))的鼓励下,我们决定用C#写一个客户端程序,正好他在学习WPF编程,于是界面采用了WPF编写。我负责后台数据交互,他负责前台界面逻辑。
&emsp;&emsp;**C#客户端还使用到了JSON格式化库<a href="https://github.com/JamesNK/Newtonsoft.Json">Newtonsoft.Json</a>,特此说明。**
<a name="design"></a>
## 设计思路
<a name="design-comunication"></a>
### 通信流程
1. 发送方(客户端或者服务器)发送数据包;
2. 接收方(服务器或者客户端)处理数据;
3. 如果需要反馈,接收方发送反馈数据包;
4. 发送方处理反馈数据(如果有)。
<a name="design-packet-struct"></a>
### 通信数据包结构
数据通信基于TCP协议,每次通信的数据包应包含【包头域+数据域】。包头域总是JSON格式、UTF-8编码的字符串,数据域由包头决定,可以为空。通信数据包结构如图。
<img src="design/message.png" alt="数据包结构图"/>
HEAD(包头域)至少包含两个参数:
* Toekn
* Operation
Token参数是一次通信的标识文本,由请求方随机生成,处理方返回数据包的Token参数应和请求方的Token参数一致(如果有返回数据)。
Operation参数指定本次通信的请求。它决定了数据包的其他数据。Operation具体行为定义见<a href="design/operations.svg">这里</a>。
本来做了设计图表,不过第一次用starUML,画了一半才发现完全是鬼画桃符,没有掌握正确的作图方式,也没什么热情重新画了。因此这里不展示完整的设计文件了(本来也不完整),上面的链接是截取的关键部分(才知道starUML可以导出html文档)。
至于原文件...留着权当做个纪念,当作自己学习的见证吧(我也很无奈啊(* ̄︿ ̄))。实在是不嫌弃不怕被误导的话,也可以看看。文件是starUML3的设计文件(.mdj),需要<a href="http://staruml.io/">starUML</a>软件打开(付费的,不过可以免费使用)。这是<a href="design/stormchat.zip">设计文件</a>。
哦,还有服务器端的数据库结构,见文件stormchat-server/res/stormchat.sql。
<a name="env-config"></a>
## 环境配置
<a name="env-config-dev"></a>
1. ### 开发环境
服务器端:Windows/Linux Golang 1.11
客户端-GoWindows x64C++Golang 1.11
客户端-C#Windows.NET4.0WPFNewtonsoft.Json for .NET4.0
<a name="env-config-run"></a>
2. ### 运行环境
服务器端:Linux/Windows, MySQL5.5+/MariaDB10.0+
客户端:Windows10 x64(其他平台没测试过)
<a name="soft-config"></a>
## 程序配置
由于懒癌发作,一些参数设置只能在编译时指定好,没有运行中指定参数的功能。
Go语言的部分(服务器程序和golang客户端程序)这块都在control.go文件中,C#客户端这边也没什么可配置的,也就是连接服务器的地址和端口。下面列出部分配置参数:
<a name="soft-config-server"></a>
### 服务器端
* **serveMode**
服务(后台)模式。如果此参数为true,日志输出到str_log_file参数指定的文件中,且Debug()函数将不输出内容。如果为false,日志输出和Debug()函数都将直接向控制台或终端输出。
* max_head_length
最大包头长度,单位字节,如果包头长度超过此参数将被抛弃。
* max_message_length
最大消息长度,单位字节,如果消息长度超过此参数将被抛弃。
* max_photo_size
最大头像大小,单位字节,如果头像数据长度超过此参数将被抛弃。
* timeout_message
客户端连接最大数据等待时间,单位秒。如果超过此时间客户端没有数据到达则断开连接(此参数当前未启用)。
* str_log_file
日志文件。仅在serveMode为true时有效。注意,程序并不会自动清理此文件。
* **str_db_conn_str**
MySQL数据库连接字符串。
<a name="soft-config-client-go"></a>
### 客户端(Go
* server_addr
服务器地址和端口。
<a name="soft-config-client-cs"></a>
### 客户端(C#
资源定义在StormChat解决方案,Interact项目的属性->资源中。
* RemoteServerAddr
服务器地址。
* RemoteServerPort
服务器端口。
<a name="build"></a>
## 如何编译
首先安装git和golang,这一步请自行解决;
克隆项目到本地:
> `git clone -b master https://github.com/mattuylee/stormchat.git`
假设项目已克隆到本地*DIR*目录,命令行切换到stormchat目录:
> `cd DIR/stormchat`
<a name="build-server"></a>
1. ### 服务器端
#### 切换到服务器端工程目录:
> `cd stormchat-server`
#### 克隆mysql操作库
Go标准库里是没有数据库操作的库的,因此先添加mysql操作库到项目中。创建目录*sotormchat-server/src/github.com/go-sql-driver/*,然后克隆MySQL操作库:
> `cd src/github.com/go-sql-driver`
> `git clone https://github.com/go-sql-driver/mysql.git`
#### 配置环境变量GOPATH
Windows下:
如果环境变量GOPATH存在,则在GOPATH后添加 ";DIR/stormchat/stormchat-server"(不含引号,注意分号),如果不存在添加GOPATH环境变量并将其设置为*DIR/stormchat/stormchat-server*即可,注意把DIR换成git仓库所在目录。
Linux下:
先查看GOPATH环境变量的值,再添加项目目录到GOPATH:
> `echo $GOPATH`
如果值为空:
> `export GOPATH=DIR/stormchat/stormchat-server`
如果值不为空:
> `export GOPATH=$GOPATH:DIR/stormchat/stormchat-server`
#### 编译程序
切换到stormchat-server/src/stormchat/目录,然后编译:
> `cd DIR/stormchat/stormchat-server/src/stormchat`
> `go build`
#### 配置MySQL
登录mysql
> `mysql -uroot -p`
为stormchat创建数据库:
> `CREATE DATABASE stormchat CHARSET=UTF8;`
导入数据库结构:
> `SOURCE DIR/stormchat/stormchat-server/res/stormchat.sql;`
创建用户并授权:
> `CREATE USER 'stormchat'@'localhost' IDENTIFIED BY 'stormchat';`
> `GRANT ALL ON stormchat.* TO 'stormchat'@'localhost';`
> `FLUSH PRIVILEGES;`
#### 启动服务器程序
Linux下执行下列命令以守护进程运行:
> `nohup ./stormchat &`
Windows请自行探索。
<a name="build-client-go"></a>
2. ### 客户端-Go
> `cd DIR/stormchat/stormchat-client-golang/src`
> `go build`
注意,**客户端运行时需要conctrl_x64.dll在运行目录**下,此文件在stormchat-client-golang/res/目录下。
<a name="build-client-cs"></a>
3. ### 客户端-C#
Visual Studio 2015以上版本打开项目,直接编译即可。
<a name="warning"></a>
## 注意事项
* 没有设计注册账户的API,只能在强插数据库。emmmm,这个坑懒得填了。
* 服务器端的日志文件不会自动清除(反正也没什么日志要写)。
* 其他的,想到再补充。
<a name="dir-struct"></a>
## 目录结构(主要部分)
<pre>
stormchat
│ .gitattributes
│ .gitignore
│ LICENSE //许可证
│ README.md //此帮助文件
├─design //设计文件
│ message.png
│ operations.svg
│ stormchat.zip //starUML设计文件
├─stormchat-server //服务器端工程目录
│ ├─res
│ │ stormchat.sql //数据库结构
│ │
│ └─src //代码文件
│ └─stormchat
│ control.go
│ err.go
│ main.go
│ message.go
│ session-deprecated.go //已不推荐使用的接口
│ session.go
│ storm-server.go
│ user.go
│─stormchat-client-golang //Go客户端工程目录
│ ├─res
│ │ conctrl_x64.dll //Winodws控制台输出库
│ │ icon.ico //客户端图标
│ │
│ └─src
│ control.go //参数控制
│ main.go
│ session.go
│ stormchat-client.syso //资源文件(图标资源)
│ user.go
│ win-console.go //输出控制,调用conctrl库
└─stormchat-client-csharp //C#客户端工程目录
├─Interact //数据交互模块
│ │ Interact.csproj //Visual Studio项目文件
│ │ Newtonsoft.Json.dll //JSON格式化库
│ │ AttrNames.cs //一些字符串常量
│ │ Client.cs //提供给数据表现模块的静态类
│ │ Client-Fields.cs //部分类,定义内部字段
│ │ Client-Handle.cs //部分类,处理服务器数据的方法
│ │ Client-Read.cs //部分类,接收服务器数据的方法
│ │ Client-Send.cs //部分类,向服务器发送数据的方法
│ │ Exception.cs
│ │ jsonObject.cs //定义一些内部使用的结构,用于JSON格式化
│ │ Message.cs //定义一些数据结构,用于数据交换
│ │ User.cs //定义用户
│ │
│ └─Properties //项目属性和资源
│ AssemblyInfo.cs
│ Resources.Designer.cs
│ Resources.resx
└─StormChatWPF //数据表现和用户交互模块*
</pre>
<a name="usage-pic"></a>
## 使用截图
### Golang客户端
<img src="pic/client-login.png"/>
<img src="pic/client-chat.png"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

@@ -4,7 +4,6 @@ using System.Linq;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Drawing;
using System.IO;
//StormClient对数据的处理相关内容
@@ -100,8 +99,7 @@ namespace Interact
};
if (int.Parse(head[AttrNames.Photo].ToString()) > 0)
{
MemoryStream ms = new MemoryStream(data);
user.Photo = Image.FromStream(ms);
user.Photo = new MemoryStream(data);
} //用户头像数据
OnLoginDone?.Invoke(resultHead, user);
}
@@ -171,8 +169,7 @@ namespace Interact
}; //基础数据
if (int.Parse(head[AttrNames.Photo].ToString()) > 0)
{
MemoryStream ms = new MemoryStream(data);
user.Photo = Image.FromStream(ms);
user.Photo = new MemoryStream(data);
} //头像数据
//加入临时用户列表缓存
User[] users;
+3 -5
View File
@@ -222,11 +222,9 @@ namespace Interact
//将头像数据转换到字符数组
if (userInfo.Photo != null)
{
MemoryStream memStream = new MemoryStream();
userInfo.Photo.Save(memStream, userInfo.Photo.RawFormat);
photoData = new byte[memStream.Length];
memStream.Seek(0, SeekOrigin.Begin);
memStream.Read(photoData, 0, photoData.Length);
photoData = new byte[userInfo.Photo.Length];
userInfo.Photo.Seek(0, SeekOrigin.Begin);
userInfo.Photo.Read(photoData, 0, photoData.Length);
}
//构建数据包
JsonUserInfoHead jsonObj = new JsonUserInfoHead()
@@ -1,71 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CEF054AF-95B7-4B88-934E-02705FEEA554}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Interact</RootNamespace>
<AssemblyName>Interact</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json">
<HintPath>.\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Client.cs" />
<Compile Include="Exception.cs" />
<Compile Include="Client-Fields.cs" />
<Compile Include="Client-Handle.cs" />
<Compile Include="jsonObject.cs" />
<Compile Include="Message.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Client-Read.cs" />
<Compile Include="Client-Send.cs" />
<Compile Include="AttrNames.cs" />
<Compile Include="User.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CEF054AF-95B7-4B88-934E-02705FEEA554}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Interact</RootNamespace>
<AssemblyName>Interact</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<Deterministic>true</Deterministic>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Newtonsoft.Json">
<HintPath>.\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Client.cs" />
<Compile Include="Exception.cs" />
<Compile Include="Client-Fields.cs" />
<Compile Include="Client-Handle.cs" />
<Compile Include="jsonObject.cs" />
<Compile Include="Message.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Client-Read.cs" />
<Compile Include="Client-Send.cs" />
<Compile Include="AttrNames.cs" />
<Compile Include="User.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
+5 -5
View File
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.IO;
namespace Interact
{
@@ -33,14 +33,14 @@ namespace Interact
}
internal static User[] Users; //用户列表。仅Interact内部访问
public static User Me; //当前用户
public static Image DefaultPhoto; //默认头像
public static User Me; //当前用户
public static Stream DefaultPhoto; //默认头像
public string Name; //用户名
public string NickName; //昵称
public string Motto; //个性签名
public UserGroup Group; //用户组
public Image Photo; //头像
public Stream Photo; //头像
}
//更新用户信息时新的用户信息。如果项值为null则不更改相应的信息
@@ -49,7 +49,7 @@ namespace Interact
public string Password = null; //新密码
public string NickName = null; //新昵称
public string Motto = null; //新签名
public Image Photo = null; //新头像
public Stream Photo = null; //新头像
}
//用户类型
@@ -4,6 +4,10 @@
xmlns:local="clr-namespace:StormChatWPF"
StartupUri="LogWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="UI/Style.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
+22 -5
View File
@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Interact;
using System.Drawing;
using System.Windows;
using System.IO;
namespace StormChatWPF
{
@@ -17,7 +18,8 @@ namespace StormChatWPF
{
StormClient.OnLoginDone += OnHaveLogin;
StormClient.OnGetUserListDone += OnGetContactsList;
StormClient.OnMessage +=OnMessage;
StormClient.OnMessage +=OnMessage;//事件挂接
User.DefaultPhoto = new MemoryStream(File.ReadAllBytes(@"../../UI/Resources/默认头像.png"));//设定默认头像
}
internal static User CurrentContact { get; set; }//设置当前联系人对象
/// <summary>
@@ -38,16 +40,21 @@ namespace StormChatWPF
mainWindow.Show();
LogWindow.Instence.Close();
});
}
}//获取联系人列表完成
private void OnHaveLogin(ResultHead head, User user)
{
if (head.Error == "")
{
{
User.Me = user;
StormClient.QueueGetUserList();
}
else
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
LogWindow.Instence.Login_button.IsEnabled = true;
});
MessageBox.Show("请核对账号密码!");
}
}//登录完成
@@ -55,7 +62,7 @@ namespace StormChatWPF
{
if (MainWindow.Instence != null)
{
MainWindow.Instence.ShowMessage(message);
MainWindow.Instence.UI_ShowMessage(message);
}
}//接受到新消息
@@ -64,7 +71,7 @@ namespace StormChatWPF
Message msg = new Message(text,target);
Action<ResultHead> f = delegate (ResultHead head)
{
MainWindow.Instence.ShowMessage(msg);
MainWindow.Instence.UI_ShowMessage(msg);
};
StormClient.QueueSendMessage(msg,f);
}//发送消息
@@ -74,11 +81,21 @@ namespace StormChatWPF
{
if (!StormClient.QueueLogin(user, password))
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
LogWindow.Instence.Login_button.IsEnabled = true;
});
MessageBox.Show("登录失败");
}
}
else
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
LogWindow.Instence.Login_button.IsEnabled = true;
});
MessageBox.Show("连接服务器失败!");
}
}//登录
@@ -21,7 +21,7 @@
<RowDefinition Height="150*"/>
</Grid.RowDefinitions>
<Image x:Name="LogBackground" Grid.ColumnSpan="4" Height="268" Margin="10,10,10,0" Grid.RowSpan="2" VerticalAlignment="Top" Source="pack://application:,,,/UI/Resources/登录界面底.png"/>
<Button x:Name="Login_button" Content="LogIn" Click="Login_button_Click" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="2" Margin="160.6,69.6,160.6,44.6" Width="70" Height="25" HorizontalAlignment="Center"/>
<Button x:Name="Login_button" Content="LogIn" Click="Login_button_Click" Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="2" Margin="165,72,166,42" Width="64" Height="25" HorizontalAlignment="Center"/>
<TextBox x:Name="AccountBox" TextWrapping="Wrap" Text="201701110203" Grid.Row="2" TextAlignment="Left" Margin="137.6,4.6,137.6,112.6" Grid.Column="1" Grid.ColumnSpan="2" Height="22" Width="122" HorizontalAlignment="Center" BorderThickness="0,0,0,1" VerticalAlignment="Top" />
<PasswordBox x:Name="passwordBox" Password="1233211234567" Margin="135,35,135,0" Grid.Column="1" Grid.Row="2" VerticalAlignment="Top" Grid.ColumnSpan="2" Height="22" Width="122" HorizontalAlignment="Center" BorderThickness="0,0,0,1" HorizontalContentAlignment="Left"/>
<Label x:Name="Account" Content="Account:" Grid.Column="1" Margin="0,138.8,70.2,0" Grid.Row="1" VerticalAlignment="Top" RenderTransformOrigin="0.415,0.379" Height="24" FontFamily="Comic Sans MS" FontStyle="Italic" Grid.RowSpan="2" HorizontalContentAlignment="Center" HorizontalAlignment="Right" Width="60"/>
@@ -1,37 +1,36 @@
using Interact;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace StormChatWPF
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class LogWindow : Window
{
public static LogWindow Instence;
public LogWindow()
{
InitializeComponent();
Instence = this;
}
private void Login_button_Click(object sender, RoutedEventArgs e)
{
Chat.Log(AccountBox.Text,passwordBox.Password);
}
}
}
using Interact;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace StormChatWPF
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class LogWindow : Window
{
public static LogWindow Instence;
public LogWindow()
{
InitializeComponent();
Instence = this;
}
private void Login_button_Click(object sender, RoutedEventArgs e)
{
Login_button.IsEnabled = false;
Chat.Log(AccountBox.Text,passwordBox.Password);
}
}
}
@@ -1,31 +1,47 @@
<Window x:Class="StormChatWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StormChatWPF"
mc:Ignorable="d"
Title="StormChat" Height="450" Width="800"
WindowStartupLocation="CenterScreen">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60*"/>
<RowDefinition Height="300*"/>
<RowDefinition Height="90*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="UsersList" Margin="0,0,0,0" IsEnabled="True" SelectionChanged="UsersList_SelectionChanged" Grid.Row="1" Grid.RowSpan="2"/>
<TextBox x:Name="InputBox" Margin="0,0,0,0" TextWrapping="Wrap" Text="" Grid.Row="2" Grid.ColumnSpan="3" Grid.Column="1" />
<Button Content="Send" HorizontalAlignment="Right" Margin="0,0,0,0" VerticalAlignment="Bottom" Width="75" Click="Button_Click" Height="20" Grid.Row="2" Grid.Column="3"/>
<ScrollViewer Grid.ColumnSpan="3" Grid.Column="1" Margin="0,0,0,0" Grid.Row="1" >
<ScrollViewer.Content>
<StackPanel x:Name="OutBox" Margin="0,0,0,0" Grid.ColumnSpan="3" Grid.Column="1" Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
</ScrollViewer.Content>
</ScrollViewer>
</Grid>
</Window>
<Window x:Class="StormChatWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StormChatWPF"
xmlns:my="clr-namespace:StormChatWPF.UI"
mc:Ignorable="d"
Title="StormChat" Height="600" Width="850"
WindowStartupLocation="CenterScreen"
Icon="UI/Resources/闪电.png">
<Grid Margin="0,0,0,0">
<Grid.RowDefinitions>
<RowDefinition Height="60*"/>
<RowDefinition Height="300*"/>
<RowDefinition Height="100*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60*"/>
<ColumnDefinition Width="140*"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
<ColumnDefinition Width="200*"/>
</Grid.ColumnDefinitions>
<ListView x:Name="UsersList" IsEnabled="True" SelectionChanged="UsersList_SelectionChanged" Grid.Row="1" Grid.RowSpan="2" HorizontalAlignment="Left" Width="206" Grid.ColumnSpan="2" Margin="0,10,0,0" />
<TextBox x:Name="InputBox" TextWrapping="Wrap" Text="" Grid.Row="2" Grid.ColumnSpan="3" Grid.Column="2" Height="109" VerticalAlignment="Bottom" Margin="10,0,10,5" />
<Button Content="Send" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" Click="Button_Click" Height="20" Grid.Row="2" Grid.Column="4" Margin="0,0,10,10"/>
<ScrollViewer Grid.ColumnSpan="3" Grid.Column="2" Grid.Row="1" BorderThickness="1">
<ScrollViewer.Content>
<StackPanel x:Name="OutBox" Margin="0,0,0,0" Grid.ColumnSpan="3" Grid.Column="1" Grid.Row="1" ScrollViewer.VerticalScrollBarVisibility="Hidden"/>
</ScrollViewer.Content>
</ScrollViewer>
<Image x:Name="User_HeadPicture" VerticalAlignment="Stretch" Margin="0,10,0,0"/>
<Label x:Name="User_NickNmae" Content="小施" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" FontSize="18" Grid.Column="1" HorizontalAlignment="Left" Width="115" Margin="5,10,0,25"/>
<Label x:Name="User_motto" Content="别寻事惹非" Margin="5,54,0,0" VerticalContentAlignment="Center" FontSize="6" Grid.Column="1" HorizontalAlignment="Left" Width="143"/>
<Image x:Name="image" Grid.Column="2" Height="54" Margin="53,10,106,10" Width="51" Source="UI/Resources/闪电.png" Visibility="Visible" Stretch="Fill">
<Image.ContextMenu>
<ContextMenu x:Name="Menu" Initialized="Menu_Initialized" MouseLeftButtonUp="Menu_MouseLeftButtonDown">
<MenuItem Header="Settings" Click="MenuItem_Settings_Click"/>
<MenuItem Header="SignOut"/>
</ContextMenu>
</Image.ContextMenu>
</Image>
</Grid>
</Window>
@@ -1,83 +1,138 @@
using Interact;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace StormChatWPF
{
/// <summary>
/// UserWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public static MainWindow Instence;
public MainWindow()
{
InitializeComponent();
LoadContactsList();
Instence = this;
}
private void LoadContactsList()
{
if (Chat.ContactsList.Any())
{
foreach (var user in Chat.ContactsList)
{
UsersList.Items.Add(user.NickName);
}
}
}//向联系人列表listview中添加元素
private void UsersList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (UsersList.SelectedItem != null)
{
Chat.CurrentContact = Chat.ContactsList[UsersList.SelectedIndex];
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (Chat.CurrentContact == null)
{
MessageBox.Show("请选择联系人!");
return;
}
if (InputBox.Text!="")
{
Chat.chat.SendMessage(InputBox.Text, Chat.CurrentContact);
InputBox.Text = "";
}
}
internal void ShowMessage(Message message)
{
if (message.To == User.Me)
{
App.Current.Dispatcher.Invoke(
delegate ()
{
OutBox.Children.Add(new ChatBubble(message,HorizontalAlignment.Left));
});
}//接受到的的消息
else
{
App.Current.Dispatcher.Invoke(
delegate ()
{
OutBox.Children.Add(new ChatBubble(message,HorizontalAlignment.Right));
});
}//发送的的消息
}//将消息展现于UI界面
}
}
using Interact;
using StormChatWPF.UI;
using System;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media.Imaging;
namespace StormChatWPF
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
delegate void LoadFinish(object sender,EventArgs e);
public static MainWindow Instence;
public MainWindow()
{
InitializeComponent();
Instence = this;
MainWindowLoad += Window_Load;
MainWindowLoad.Invoke(this,null);
}
void Window_Load(object sender, EventArgs e)
{
UI_LoadContactsList();
UI_LoadUserMe();
}
private void UI_LoadUserMe()
{
BitmapImage image = new BitmapImage();
image.BeginInit();
image.StreamSource = User.Me.Photo;
User.Me.Photo.Seek(0, SeekOrigin.Begin);
image.EndInit();
User_HeadPicture.Source = image;
User_NickNmae.Content = User.Me.NickName;
User_motto.Content = User.Me.Motto;
}//加载用当前用户UI显示信息
private void UI_LoadSettings()
{
}
internal void UI_ShowMessage(Message message)
{
if (message.To == User.Me)
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
OutBox.Children.Add(new StormChatWPF.UI.ChatBubble(message, HorizontalAlignment.Left));
});
}//接受到的的消息
else
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
OutBox.Children.Add(new StormChatWPF.UI.ChatBubble(message, HorizontalAlignment.Right));
});
}//发送的的消息
}//将消息展现于UI界面
private void UI_LoadContactsList()
{
if (Chat.ContactsList.Any())
{
foreach (var user in Chat.ContactsList)
{
if (user.Name !=User.Me.Name)
{
UsersList.Items.Add(new UI.ContactsInfo(user)
{
HorizontalAlignment = HorizontalAlignment.Center
});
}
}
}
}//向联系人列表listview中添加元素
private void UsersList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (UsersList.SelectedItem != null)
{
Chat.CurrentContact = Chat.ContactsList[UsersList.SelectedIndex];
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
if (Chat.CurrentContact == null)
{
MessageBox.Show("请选择联系人!");
return;
}
if (InputBox.Text != "")
{
Chat.chat.SendMessage(InputBox.Text, Chat.CurrentContact);
InputBox.Text = "";
}
}
private void button_Click_1(object sender, RoutedEventArgs e)
{
App.Current.Dispatcher.Invoke(
(Action)delegate ()
{
});
}
event LoadFinish MainWindowLoad;
private void MenuItem_Settings_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Settings");
}
private void Menu_Initialized(object sender, EventArgs e)
{
this.image.ContextMenu = null;
}
private void Menu_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
this.Menu.PlacementTarget = this.image;
//位置
this.Menu.Placement = PlacementMode.Top;
//显示菜单
this.Menu.IsOpen = true;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -1,124 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CF4D6BE0-A862-401F-A3A6-4FDAC32F5B70}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>StormChatWPF</RootNamespace>
<AssemblyName>StormChatWPF</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject>StormChatWPF.App</StartupObject>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Chat.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
<Compile Include="ScrollViewer.cs" />
<Compile Include="UI\ChatBubble.cs" />
<Page Include="LogWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="LogWindow.xaml.cs">
<DependentUpon>LogWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="MainWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Interact\Interact.csproj">
<Project>{cef054af-95b7-4b88-934e-02705feea554}</Project>
<Name>Interact</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\登录界面底.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\闪电.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\聊天气泡.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{CF4D6BE0-A862-401F-A3A6-4FDAC32F5B70}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>StormChatWPF</RootNamespace>
<AssemblyName>StormChatWPF</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject>StormChatWPF.App</StartupObject>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="Chat.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
<Compile Include="ScrollViewer.cs" />
<Compile Include="UI\ChatBubble.cs" />
<Compile Include="UI\Contacts.xaml.cs">
<DependentUpon>Contacts.xaml</DependentUpon>
</Compile>
<Compile Include="UI\ContactsInfo.cs" />
<Compile Include="UI\DropDownMenu.xaml.cs">
<DependentUpon>DropDownMenu.xaml</DependentUpon>
</Compile>
<Compile Include="Window1.xaml.cs">
<DependentUpon>Window1.xaml</DependentUpon>
</Compile>
<Page Include="LogWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="LogWindow.xaml.cs">
<DependentUpon>LogWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="MainWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Contacts.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\DropDownMenu.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="UI\Style.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Window1.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<None Include="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Interact\Interact.csproj">
<Project>{cef054af-95b7-4b88-934e-02705feea554}</Project>
<Name>Interact</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\登录界面底.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\闪电.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\聊天气泡.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Resource Include="UI\Resources\默认头像.png" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -5,7 +5,7 @@ using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace StormChatWPF
namespace StormChatWPF.UI
{
/// <summary>
/// 聊天气泡
@@ -23,7 +23,7 @@ namespace StormChatWPF
VerticalAlignment = VerticalAlignment.Bottom
};
/// <summary>
///
/// 生成聊天气泡
/// </summary>
/// <param name="msg">传入消息体</param>
/// <param name="alignment">设置水平对齐方式,参数为HorizontalAlignment枚举类型</param>
@@ -0,0 +1,17 @@
<UserControl x:Class="StormChatWPF.UI.Contacts"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StormChatWPF.UI"
mc:Ignorable="d"
d:DesignHeight="40" d:DesignWidth="120">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="40*"/>
<ColumnDefinition Width="80*"/>
</Grid.ColumnDefinitions>
<Image x:Name="HeadPicture" HorizontalAlignment="Left" Height="40" Width="40"/>
<Label x:Name="text" Content="Label" Height="40" FontSize="14" HorizontalContentAlignment="Center" VerticalContentAlignment="Center" HorizontalAlignment="Right" Grid.Column="1" Width="80"/>
</Grid>
</UserControl>
@@ -0,0 +1,30 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace StormChatWPF.UI
{
/// <summary>
/// Contacts.xaml 的交互逻辑
/// </summary>
public partial class Contacts : UserControl
{
public Contacts()
{
InitializeComponent();
var metadata = new FrameworkPropertyMetadata((ImageSource)null);
}
public Image Head
{
get
{
return this.HeadPicture;
}
set
{
this.HeadPicture = value;
}
}
}
}
@@ -0,0 +1,21 @@
using Interact;
using System.Windows.Media.Imaging;
using System.IO;
namespace StormChatWPF.UI
{
internal class ContactsInfo : Contacts
{
public ContactsInfo(User user)
{
image.BeginInit();
image.StreamSource = user.Photo;
User.DefaultPhoto.Seek(0, SeekOrigin.Begin);
image.EndInit();
HeadPicture.Source = image;
text.Content = user.NickName;
Width = 120;
}
BitmapImage image = new BitmapImage();
}
}
@@ -0,0 +1,13 @@
<UserControl x:Class="StormChatWPF.UI.DropDownMenu"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:StormChatWPF.UI"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="30">
<Grid>
<ComboBox x:Name="comboBox" Style="{StaticResource DropDownMenu}" Margin="-48,-7,37,-4"/>
<Image x:Name="image" HorizontalAlignment="Left" Height="30" Margin="-44,-40,0,0" VerticalAlignment="Top" Width="30"/>
</Grid>
</UserControl>
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace StormChatWPF.UI
{
/// <summary>
/// DropDownMenu.xaml 的交互逻辑
/// </summary>
public partial class DropDownMenu : UserControl
{
public DropDownMenu()
{
InitializeComponent();
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

@@ -0,0 +1,16 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StormChatWPF.UI">
<Style x:Key="DropDownMenu" TargetType="ComboBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid Height="100" Width="100" Margin="0,0,0,0">
<ComboBox Width="Auto" Height="Auto" VerticalAlignment="Top" Margin="30,10,30,0" Visibility="Hidden"/>
<Image Width="Auto" Height="Auto" Source="pack://application:,,,/UI/Resources/闪电.png" Stretch="Fill" Margin="0" VerticalAlignment="Stretch"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -0,0 +1,12 @@
<Window x:Class="StormChatWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:StormChatWPF"
mc:Ignorable="d"
Title="Window1" Height="300" Width="300">
<Grid>
<ComboBox x:Name="comboBox" HorizontalAlignment="Left" Margin="75,85,0,0" VerticalAlignment="Top" Width="120" Style="{DynamicResource DropDownMenu}"/>
</Grid>
</Window>
@@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace StormChatWPF
{
/// <summary>
/// Window1.xaml 的交互逻辑
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e)
{
}
}
}
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

-52
View File
@@ -1,52 +0,0 @@
package main
//宏
const (
//最大包头长度
max_head_length uint32 = 4096 //4K
//最大消息长度
max_message_length uint32 = 0x6400000 //64M
//最大头像大小
max_photo_size uint32 = 0x400000 //4M
//服务器地址
server_addr string = "localhost:3727"
)
//消息头的通用字段名称
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
@@ -1,256 +0,0 @@
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
@@ -1,324 +0,0 @@
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("连接服务器失败:" + err.Error())
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.
-17
View File
@@ -1,17 +0,0 @@
package main
//用户信息
type UserInfo struct {
User string //用户名
NickName string //昵称
Motto string //签名
UGroup string //用户组。ugroup_*常量值
}
//用户组
const (
ugroup_user string = "User" //普通用户
ugroup_vip string = "Vip" //会员
ugroup_admin string = "Admin" //管理员
ugroup_group string = "Group" //群聊
)
-158
View File
@@ -1,158 +0,0 @@
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
@@ -1,28 +0,0 @@
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`)
) 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`)
) DEFAULT CHARSET=utf8;
LOCK TABLES `user` WRITE;
INSERT INTO `user` VALUES ('test','test','Test','This is my Motto.','Admin',NULL);
UNLOCK TABLES;
-45
View File
@@ -1,45 +0,0 @@
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 (
//日志文件。交互模式时无效(输出到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
@@ -1,19 +0,0 @@
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)
}
-47
View File
@@ -1,47 +0,0 @@
//主程序
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)
Debug("Server Working")
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
}
}
}
-111
View File
@@ -1,111 +0,0 @@
package main
import (
"encoding/base64"
"time"
)
//控制消息。详见设计文档
const (
operation_panic string = "Panic"
operation_trans_message string = "TransMessage"
operation_send_message string = "SendMessage"
operation_ping string = "Ping"
operation_login string = "Login2"
operation_login_old1 string = "Login"
operation_logout string = "Logout"
operation_offline string = "Offline"
operation_get_user_list string = "GetUserList"
operation_update_userinfo string = "UpdateUserInfo"
operation_get_users string = "GetUsers"
)
//消息头的通用字段名称
const (
keyname_operation string = "Operation" //操作名
keyname_token string = "Token"
)
//消息结构
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
}
@@ -1,58 +0,0 @@
//session已不推荐使用的的接口
package main
import (
"encoding/json"
)
//登录事件处理函数。已弃用,请使用LoginHandler
func (session *Session) Old1_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.")
}
//获取用户列表。已废弃,请使用GetUsers接口
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)
}
-478
View File
@@ -1,478 +0,0 @@
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 && headInfo[keyname_operation] != operation_login_old1 {
returnHead := make(map[string]string)
returnHead[keyname_token] = headInfo[keyname_token]
returnHead[keyname_operation] = headInfo[keyname_operation]
returnHead["Error"] = "Permission failed. Please sign in."
session.SendData(returnHead, nil)
continue
} //抛弃未登录用户的消息
switch headInfo[keyname_operation] {
case operation_send_message:
go session.MessageHandler(headInfo, data)
case operation_ping:
go session.PingHandler()
case operation_login_old1: //Deprecated
session.Old1_LoginHandler(headInfo)
case operation_login:
session.LoginHandler(headInfo)
case operation_logout:
session.LogoutHandler()
case operation_get_user_list:
go session.GetUserListHandler(headInfo)
case operation_update_userinfo:
go session.UpdateUserInfoHandler(headInfo, data)
case operation_get_users:
go session.GetUsersHandler(headInfo)
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"] = ""
var user *UserInfo //用户信息
//已经登录
if session.status != session_status_created {
user = session.sender
session.SendData(returnInfo, nil)
} else {
//登录
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
}
returnInfo["User"] = user.User
returnInfo["NickName"] = user.NickName
returnInfo["Motto"] = user.Motto
returnInfo["UGroup"] = user.UGroup
var photo []byte
if username, ok := headInfo["User"]; ok {
photo = GetUserPhoto(username)
} else {
photo = GetUserPhoto(session.sender.User)
}
returnInfo["Photo"] = strconv.Itoa(len(photo))
session.SendData(returnInfo, photo)
//转发用户未读消息
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 needResult, ok := headInfo["NeedResult"]; ok && needResult != "0" {
session.SendData(result, nil)
} //返回执行结果
}
//更改用户信息
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)
}
//获取(指定/所有)用户信息
func (session *Session) GetUsersHandler(headInfo map[string]string) {
result := make(map[string]string)
result[keyname_token] = headInfo[keyname_token]
result[keyname_operation] = headInfo[keyname_operation]
result["Error"] = ""
users := make([]UserInfo, 1) //用户列表
//获取特定用户信息
if username, ok := headInfo["User"]; ok {
user := QueryUserInfo(username)
if user != nil {
result["Error"] = "User '" + headInfo["User"] + "' don't exists."
session.SendData(result, nil)
return
} else {
users = append(users, *user)
}
//获取所有用户信息
} else {
users = GetUserList()
}
//发送用户数据
var photoData []byte //头像数据
result["Error"] = ""
result["Total"] = strconv.Itoa(len(users)) //总用户数
for i, curUser := range users {
photoData = GetUserPhoto(curUser.User)
result["Count"] = strconv.Itoa(i + 1) //当前用户次序编号
result["User"] = curUser.User
result["NickName"] = curUser.NickName
result["Motto"] = curUser.Motto
result["UGroup"] = curUser.UGroup
result["Photo"] = strconv.Itoa(len(photoData)) //头像数据长度
session.SendData(result, photoData)
}
//发送用户列表结束封包
endingResult := make(map[string]string)
endingResult[keyname_token] = result[keyname_token]
endingResult[keyname_operation] = result[keyname_operation]
endingResult["Total"] = result["Total"]
endingResult["Count"] = strconv.Itoa(-1)
endingResult["Error"] = ""
session.SendData(endingResult, 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
}
@@ -1,59 +0,0 @@
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()
}
}
-141
View File
@@ -1,141 +0,0 @@
package main
import (
"encoding/base64"
"unicode/utf8"
)
//用户组
const (
ugroup_user string = "User" //普通用户
ugroup_vip string = "Vip" //会员
ugroup_admin string = "Admin" //管理员
ugroup_group string = "Group" //群聊
)
//用户信息
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 ""
}