Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b80ed17e7 | |||
| 708e427ac1 | |||
| d075b7a07a | |||
| 241fe050fd | |||
| d9445b707e | |||
| bac637213b | |||
| 1f71eb11f2 | |||
| c3d163dfa8 | |||
| 0bc90a26f4 | |||
| 7711746c14 | |||
| 2ec9e9f763 | |||
| 61794c0b20 | |||
| 1063dbfc84 | |||
| f5b27456dd | |||
| 64e54bfe52 | |||
| bbd6bff4aa | |||
| dd3160174c | |||
| 6d05a3d04e | |||
| 59389fdca3 | |||
| f27f629d53 | |||
| 7c3873c5b5 | |||
| f3df3b2995 | |||
| 4ec1c43879 | |||
| d854d27817 | |||
| 1881936429 | |||
| f8e4af399a | |||
| 91df10beb6 | |||
| 51e8622c1c | |||
| f7006ad5c1 | |||
| d6285ca846 | |||
| 5bc6f9047e | |||
| fb8136cc92 | |||
| a7440380c8 | |||
| 88053b0cd4 | |||
| a3ebaca234 | |||
| 98bed05764 | |||
| cee5d98df3 | |||
| d8e810a5ed | |||
| a3ed62853a | |||
| 84052a0622 | |||
| d20536e09a | |||
| 3003a8868a | |||
| 7b06ef8dea | |||
| 1655b92d9a | |||
| c43d9a1a75 | |||
| 5e6fb470a5 |
@@ -0,0 +1,285 @@
|
||||
# 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>
|
||||
## 简介
|
||||
  前段时间心血来潮想学习最近的明星编程语言Golang。于是想做个聊天小程序实践一下。程序基于TCP协议通信,更详细的设计见<a href="#design">设计思路</a>。
|
||||
|
||||
  用Golang实现了服务端程序,同时代码抄抄改改做了个Golang客户端,又由于输出问题,用C++写了个Windows控制台的简陋的[输出控制库](https://github.com/mattuylee/conctrl),于是客户端在Windows上能看了(不过朋友说很丑>_<)。
|
||||
|
||||
  在朋友([@Billows](https://github.com/KaniuBillows))的鼓励下,我们决定用C#写一个客户端程序,正好他在学习WPF编程,于是界面采用了WPF编写。我负责后台数据交互,他负责前台界面逻辑。
|
||||
|
||||
  **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
|
||||
客户端-Go:Windows x64,C++,Golang 1.11
|
||||
客户端-C#:Windows,.NET4.0,WPF,Newtonsoft.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.
|
After Width: | Height: | Size: 9.2 KiB |
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 77 KiB |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
|
||||
//StormClient对数据的处理相关内容
|
||||
@@ -99,7 +100,8 @@ namespace Interact
|
||||
};
|
||||
if (int.Parse(head[AttrNames.Photo].ToString()) > 0)
|
||||
{
|
||||
user.Photo = new MemoryStream(data);
|
||||
MemoryStream ms = new MemoryStream(data);
|
||||
user.Photo = Image.FromStream(ms);
|
||||
} //用户头像数据
|
||||
OnLoginDone?.Invoke(resultHead, user);
|
||||
}
|
||||
@@ -169,7 +171,8 @@ namespace Interact
|
||||
}; //基础数据
|
||||
if (int.Parse(head[AttrNames.Photo].ToString()) > 0)
|
||||
{
|
||||
user.Photo = new MemoryStream(data);
|
||||
MemoryStream ms = new MemoryStream(data);
|
||||
user.Photo = Image.FromStream(ms);
|
||||
} //头像数据
|
||||
//加入临时用户列表缓存
|
||||
User[] users;
|
||||
|
||||
@@ -222,9 +222,11 @@ namespace Interact
|
||||
//将头像数据转换到字符数组
|
||||
if (userInfo.Photo != null)
|
||||
{
|
||||
photoData = new byte[userInfo.Photo.Length];
|
||||
userInfo.Photo.Seek(0, SeekOrigin.Begin);
|
||||
userInfo.Photo.Read(photoData, 0, photoData.Length);
|
||||
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);
|
||||
}
|
||||
//构建数据包
|
||||
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>
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Drawing;
|
||||
|
||||
namespace Interact
|
||||
{
|
||||
@@ -33,14 +33,14 @@ namespace Interact
|
||||
}
|
||||
internal static User[] Users; //用户列表。仅Interact内部访问
|
||||
|
||||
public static User Me; //当前用户
|
||||
public static Stream DefaultPhoto; //默认头像
|
||||
public static User Me; //当前用户
|
||||
public static Image DefaultPhoto; //默认头像
|
||||
|
||||
public string Name; //用户名
|
||||
public string NickName; //昵称
|
||||
public string Motto; //个性签名
|
||||
public UserGroup Group; //用户组
|
||||
public Stream Photo; //头像
|
||||
public Image Photo; //头像
|
||||
}
|
||||
|
||||
//更新用户信息时新的用户信息。如果项值为null则不更改相应的信息
|
||||
@@ -49,7 +49,7 @@ namespace Interact
|
||||
public string Password = null; //新密码
|
||||
public string NickName = null; //新昵称
|
||||
public string Motto = null; //新签名
|
||||
public Stream Photo = null; //新头像
|
||||
public Image Photo = null; //新头像
|
||||
}
|
||||
|
||||
//用户类型
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
xmlns:local="clr-namespace:StormChatWPF"
|
||||
StartupUri="LogWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="UI/Style.xaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
|
||||
@@ -6,7 +6,6 @@ using System.Threading.Tasks;
|
||||
using Interact;
|
||||
using System.Drawing;
|
||||
using System.Windows;
|
||||
using System.IO;
|
||||
|
||||
namespace StormChatWPF
|
||||
{
|
||||
@@ -18,8 +17,7 @@ namespace StormChatWPF
|
||||
{
|
||||
StormClient.OnLoginDone += OnHaveLogin;
|
||||
StormClient.OnGetUserListDone += OnGetContactsList;
|
||||
StormClient.OnMessage +=OnMessage;//事件挂接
|
||||
User.DefaultPhoto = new MemoryStream(File.ReadAllBytes(@"../../UI/Resources/默认头像.png"));//设定默认头像
|
||||
StormClient.OnMessage +=OnMessage;
|
||||
}
|
||||
internal static User CurrentContact { get; set; }//设置当前联系人对象
|
||||
/// <summary>
|
||||
@@ -40,21 +38,16 @@ 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("请核对账号密码!");
|
||||
}
|
||||
}//登录完成
|
||||
@@ -62,7 +55,7 @@ namespace StormChatWPF
|
||||
{
|
||||
if (MainWindow.Instence != null)
|
||||
{
|
||||
MainWindow.Instence.UI_ShowMessage(message);
|
||||
MainWindow.Instence.ShowMessage(message);
|
||||
}
|
||||
}//接受到新消息
|
||||
|
||||
@@ -71,7 +64,7 @@ namespace StormChatWPF
|
||||
Message msg = new Message(text,target);
|
||||
Action<ResultHead> f = delegate (ResultHead head)
|
||||
{
|
||||
MainWindow.Instence.UI_ShowMessage(msg);
|
||||
MainWindow.Instence.ShowMessage(msg);
|
||||
};
|
||||
StormClient.QueueSendMessage(msg,f);
|
||||
}//发送消息
|
||||
@@ -81,21 +74,11 @@ 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="165,72,166,42" Width="64" 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="160.6,69.6,160.6,44.6" Width="70" 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,36 +1,37 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,31 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@@ -1,138 +1,83 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
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界面
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,153 +1,124 @@
|
||||
<?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" />
|
||||
<?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" />
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@ using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace StormChatWPF.UI
|
||||
namespace StormChatWPF
|
||||
{
|
||||
/// <summary>
|
||||
/// 聊天气泡
|
||||
@@ -23,7 +23,7 @@ namespace StormChatWPF.UI
|
||||
VerticalAlignment = VerticalAlignment.Bottom
|
||||
};
|
||||
/// <summary>
|
||||
/// 生成聊天气泡
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="msg">传入消息体</param>
|
||||
/// <param name="alignment">设置水平对齐方式,参数为HorizontalAlignment枚举类型</param>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<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>
|
||||
@@ -1,30 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<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>
|
||||
@@ -1,27 +0,0 @@
|
||||
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.
|
Before Width: | Height: | Size: 135 KiB |
@@ -1,16 +0,0 @@
|
||||
<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>
|
||||
@@ -1,12 +0,0 @@
|
||||
<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>
|
||||
@@ -1,31 +0,0 @@
|
||||
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.
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,52 @@
|
||||
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 强制退出
|
||||
|
||||
`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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("连接服务器失败:" + 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.
@@ -0,0 +1,17 @@
|
||||
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" //群聊
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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`)
|
||||
) 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;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
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`=?"
|
||||
)
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//主程序
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//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)
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
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 ""
|
||||
}
|
||||
Reference in New Issue
Block a user