01-课程介绍和仿QQ软件设计
基于.NET Winform
可以多用户聊天
支持注册
需要登录
支持加好友
支持文本聊天
使用数据库
软件的设计流程图
基于.NET 4.0
基于三层架构
基于ADO.NET
使用动软代码生成器
程序的流程图
02-搭建环境-无边框窗体
创建一个windows项目 使用VS2015 使用WPF
WPF去掉顶部栏,用来做登录和注册窗口
设置窗口无标题无边框
Title="QQ登录"
Height="250"
Width="350"
WindowStyle="None"Code language: JavaScript (javascript)
设置不能设置长宽
ResizeMode="NoResize"Code language: JavaScript (javascript)
设置窗口可以鼠标移动
给window属性添加事件绑定方法
MouseLeftButtonDown="Window_MouseLeftButtonDown"
private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}Code language: JavaScript (javascript)
03-设置登录窗体的背景图片和最小化和关闭按钮
我们设计或者下载一张图片 用来作为登录框的背景颜色。
我们这些都是参考QQ的界面的。
然后我们再项目中添加一个文件夹Images 然后把图片拷贝进去
然后添加如下代码
<Window.Background>
<ImageBrush ImageSource="Images/background.png"/>
</Window.Background>Code language: HTML, XML (xml)
添加布局增加最小化和关闭按钮
使用DockPanel来布局
<DockPanel LastChildFill="False">
<DockPanel Background="Transparent" DockPanel.Dock="Top" Height="25" LastChildFill="False">
<TextBlock Text="QQ登录" Foreground="White" FontSize="12" Padding="5" DockPanel.Dock="Left"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Background="Transparent" HorizontalAlignment="Right">
<Button Content=" - " Foreground="White" Background="Transparent" BorderThickness="0" Click="Button_Click_1" Margin="0,5,15,2"></Button>
<Button Content=" x " Foreground="White" Background="Transparent" BorderThickness="0" Click="Button_Click" Margin="0,5,5,2"></Button>
</StackPanel>
</DockPanel>
</DockPanel>Code language: HTML, XML (xml)
关闭和最小化按钮事件方法
private void Button_Click_1(object sender, RoutedEventArgs e)
{
this.WindowState = WindowState.Minimized;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.Close();
}Code language: JavaScript (javascript)
04-设计登录框底部的按钮
在登录框的底部 有登录 取消 和没有账号这三个按钮。我们来设计一下 这几个按钮。
WPF中的magrin属性 分别对应左上右下
Margin="0,10,10,10"Code language: JavaScript (javascript)
其中还没账号按钮是跳转到注册页面 所以我们给他绑定下面的按钮事件
窗体间的跳转
private void Button_Click_2(object sender, RoutedEventArgs e)
{
//this.Hide();
//MainWindow dlg = new MainWindow();
//dlg.ShowDialog();
this.Close();
}Code language: JavaScript (javascript)
设置窗体的打开位置
WindowStartupLocation="CenterScreen"Code language: JavaScript (javascript)
设置界面的左侧显示一张图片 右侧显示一个矩形控件
<Image Width="100" DockPanel.Dock="Left" Source="/images/qq.png"/>
<Rectangle Width="30" DockPanel.Dock="Right"/>Code language: HTML, XML (xml)
05-登录界面输入框的设计
输入框是显示在中间的。
我们使用Grid控件来进行布局
中间区域是全屏的 所以我们要把<DockPanel LastChildFill=”True”>设为true
QQ账号输入框使用TextBox控件 然后密码使用PasswordBox
文本框的内容垂直居中
VerticalContentAlignment="Center"Code language: JavaScript (javascript)
Grid代码
<Grid>
<!--定义了两列-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!--定义了两行-->
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" TextAlignment="Right" Foreground="White" Padding="0,2,0,0" Text="QQ号:" Name="UserName" Height="22"/>
<TextBlock Grid.Row="1" Grid.Column="0" TextAlignment="Right" Foreground="White" Padding="0,2,0,0" Text="密码:" Name="pass" Height="22"/>
<TextBox VerticalContentAlignment="Center" Name="TxtBoxName" Background="White" Grid.Row="0" Grid.Column="1" Text="" Height="22" />
<PasswordBox VerticalContentAlignment="Center" Name="pass1" Background="White" Grid.Row="1" Grid.Column="1" Height="22"/>
</Grid>Code language: HTML, XML (xml)
06-快速实现注册界面
由于注册界面和登录界面是差不多。所以我们通过登录界面,快速实现注册界面。
注册界面区别就是把QQ号改成QQ昵称。然后添加一个确认密码。同时把未有账号 改为已有账号 然后点击事件 改为跳转到注册框。
区别就是中间区域了。
<Grid Width="300" Height="200" >
<!--定义了两列-->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!--定义了三行-->
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" TextAlignment="Right" Foreground="White" Padding="0,2,0,0" Text="QQ昵称:" Name="UserName" Height="22"/>
<TextBlock Grid.Row="1" Grid.Column="0" TextAlignment="Right" Foreground="White" Padding="0,2,0,0" Text="密码:" Name="pass" Height="22"/>
<TextBlock Grid.Row="2" Grid.Column="0" TextAlignment="Right" Foreground="White" Padding="0,2,0,0" Text="确认密码:" Name="repass" Height="22"/>
<TextBox VerticalContentAlignment="Center" Name="TxtBoxName" Background="White" Grid.Row="0" Grid.Column="1" Text="" Height="22" />
<PasswordBox VerticalContentAlignment="Center" Name="pass1" Background="White" Grid.Row="1" Grid.Column="1" Height="22"/>
<PasswordBox VerticalContentAlignment="Center" Name="pass2" Background="White" Grid.Row="2" Grid.Column="1" Height="22" />
</Grid>Code language: HTML, XML (xml)
07-QQ聊天界面的设计
创建一个WindowChat窗体。
我们来看一下我们这个聊天框大概是怎么样的。
我们的这个界面是一个顶部 右侧 和中间的布局设计。没有底部 没有左侧。
先设置顶部
<DockPanel LastChildFill="False" Background="White">
<StackPanel Orientation="Vertical" Background="#477295" DockPanel.Dock="Top" Height="120">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Background="Transparent" HorizontalAlignment="Right">
<Button Content=" - " Foreground="White" Background="Transparent" BorderThickness="0" Click="Button_Click_1" Margin="0,5,15,2"></Button>
<Button Content=" x " Foreground="White" Background="Transparent" BorderThickness="0" Click="Button_Click" Margin="0,5,5,2"></Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Image Width="100" Height="100" Source="images/qq.png"></Image>
<StackPanel Orientation="Vertical">
<TextBlock Text="潘小姐 AndyBaby~~~" Foreground="White" Margin="0,25,0,0"></TextBlock>
<TextBlock Text="快乐开心每一天" Foreground="#CDE4EB" Margin="0,10"></TextBlock>
</StackPanel>
</StackPanel>
</StackPanel>
</DockPanel>Code language: HTML, XML (xml)
08-设置聊天框的右侧广告栏
右侧广告栏 其实真正的QQ是显示qq秀或者一些其他的广告图片等。
我们这里右侧是显示一个Border控件的 里面包裹着一个StackPanel 这样我们采坑设置广告栏和中间区域的一个分割线。
设置边框控件的颜色是BorderBrush不是Background
BorderBrush=”#E8F3F9″ 然后设置边框的粗细 可以一个值 也可以四个值 分别代表 左上右下
BorderThickness=”1,0,0,0″
我们还需要一张图片
是广告图
<Border CornerRadius="1" DockPanel.Dock="Right" BorderBrush="#E8F3F9" BorderThickness="1,0,0,0">
<StackPanel Width="200" Background="Transparent">
<Image Source="images/qqshow.png"></Image>
</StackPanel>
</Border>Code language: HTML, XML (xml)
09-聊天窗口中间区域编辑框和信息框的设计
我们这里的编辑框只是修饰而已。并没有功能。如果感兴趣的同学 可以去把它完成了。
我们的编辑框的工具栏 是显示一些带小图标的按钮
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="file_font.png"/>
</Button.Background>
</Button>Code language: HTML, XML (xml)
输入框支持到右侧边框的时候 换行
<TextBox Name="TxtBoxName" Padding="8" Background="White" BorderThickness="0" Height="110" AcceptsReturn="True" TextWrapping="Wrap" />Code language: HTML, XML (xml)
底部的按钮 我们通过设置第一个按钮的左侧边距 来让它看上去是靠右的
<StackPanel Background="#E8F3F9" Orientation="Horizontal" Height="30">
<Button x:Name="button" Content="发送" Width="75" Margin="285,3,10,3"/>
<Button x:Name="button1" Content="取消" Width="75" Margin="0,3,3,3"/>
</StackPanel>
<StackPanel Orientation="Vertical" Background="Transparent" >
<StackPanel Background="White" Height="280">
</StackPanel>
<StackPanel Background="#E8F3F9" Orientation="Horizontal" Height="25">
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="images/file_font.png"/>
</Button.Background>
</Button>
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="images/color_wheel.png"/>
</Button.Background>
</Button>
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="images/emotion_bigsmile.png"/>
</Button.Background>
</Button>
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="images/picture_icon.png"/>
</Button.Background>
</Button>
<Button Width="30" Margin="3" BorderThickness="0">
<Button.Background>
<ImageBrush ImageSource="images/upload2.png"/>
</Button.Background>
</Button>
</StackPanel>
<StackPanel Background="White" Height="96">
<TextBox Name="TxtBoxName" Padding="8" Background="White" BorderThickness="0" Height="90" AcceptsReturn="True" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Background="#E8F3F9" Orientation="Horizontal" Height="30">
<Button x:Name="button" Content="发送" Width="75" Margin="285,3,10,3"/>
<Button x:Name="button1" Content="取消" Width="75" Margin="0,3,3,3"/>
</StackPanel>
</StackPanel>Code language: HTML, XML (xml)
设置聊天框的QQ好友的头像
可以设置长宽的
<Image Width="60" Height="60" Margin="10" Source="images/avatar.jpg"></Image>Code language: HTML, XML (xml)
10-设计QQ主窗体
我们根据QQ2009的版本的主窗体为设计参考。
QQ主窗体和其他的窗体的设计差不多。主题也是使用DockPanel 然后结合stackpanel 就可以完成设计了。不过在QQ主窗体中我们的QQ好友和分类 使用了Button 容器
Button本身就是一个容器。所以可以在里面添加图片 和文字。这样可以实现按钮同时带有图片和文字。
<Button Background="Transparent" Height="30" BorderThickness="0">
<StackPanel Orientation="Horizontal" Width="240" >
<Image Width="15" Height="15" Source="images/player-play.png" Margin="3"/>
<TextBlock VerticalAlignment="Center">我的好友[32/123]</TextBlock>
</StackPanel>
</Button>Code language: HTML, XML (xml)
搜索框使用了style 让它有一定的弧度
<TextBox VerticalContentAlignment="Center" Name="TxtBoxName" Background="White" Text="" Width="240" Height="22" Margin="3">
<TextBox.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="9"/>
<Setter Property="BorderBrush" Value="#c1d0dc"/>
</Style>
</TextBox.Resources>
</TextBox>Code language: HTML, XML (xml)
11-设计这个添加好友的窗体
点击主窗体底部按钮。可以弹出一个添加好友的窗体。
然后输入QQ号 然后就可以发送好友申请了。
12-添加好友窗体的打开
点击主窗体底部的添加好友按钮 可以打开一个添加好友的窗体。
添加好友窗体就不需要隐藏主窗体了
13-好友申请处理窗体
当有好友给你发送了好友申请的时候。会把该好友的头像和QQ昵称已经QQ号 显示在一个列表中。当点击同意按钮后 则成为好友
14-QQ服务器端控制台程序和EF 环境安装
服务器端,我们使用控制台程序就可以了。
甚至使用windows服务也是可以的。
当然 使用桌面程序也是可以。不过建议使用一些轻量级的就可以。
当然我们无法和真实的QQ相比的。不过其实核心的设计思路应该也是差不多的。所以我们这里使用控制台来实现就可以了
EF不一定只能用在web项目上面的。其实其他的模式下面也是可以使用的。它只不过是一个数据操作框架而已。
15-数据库的设计-EF-DataFirst
再EDMX下面 设计数据库
我们需要User表 来记录注册QQ的账号
需要FriendReleate表 来记录好友关系
然后还需要一张表来记录聊天信息 我们给它起名为Message
然后我们设计了这三张表中的字段
然后我们映射到数据库中。
-- --------------------------------------------------
-- Entity Designer DDL Script for SQL Server 2005, 2008, 2012 and Azure
-- --------------------------------------------------
-- Date Created: 06/25/2018 16:45:20
-- Generated from EDMX file: H:\.NET 开发的访QQ聊天工具\QQService\QQService\Models.edmx
-- --------------------------------------------------
SET QUOTED_IDENTIFIER OFF;
GO
USE [QQ];
GO
IF SCHEMA_ID(N'dbo') IS NULL EXECUTE(N'CREATE SCHEMA [dbo]');
GO
-- --------------------------------------------------
-- Dropping existing FOREIGN KEY constraints
-- --------------------------------------------------
-- --------------------------------------------------
-- Dropping existing tables
-- --------------------------------------------------
-- --------------------------------------------------
-- Creating all tables
-- --------------------------------------------------
-- Creating table 'UserSet'
CREATE TABLE [dbo].[UserSet] (
[Id] int IDENTITY(1,1) NOT NULL,
[Niname] nvarchar(25) NOT NULL,
[Signal] nvarchar(80) NOT NULL,
[Mumber] nvarchar(15) NOT NULL,
[Password] nvarchar(20) NOT NULL,
[AddTime] datetime NOT NULL
);
GO
-- Creating table 'FriendReleateSet'
CREATE TABLE [dbo].[FriendReleateSet] (
[Id] int IDENTITY(1,1) NOT NULL,
[FromUserId] int NOT NULL,
[ToUserId] int NOT NULL,
[IsPass] bit NOT NULL,
[AddTime] datetime NOT NULL
);
GO
-- Creating table 'MessageSet'
CREATE TABLE [dbo].[MessageSet] (
[Id] int IDENTITY(1,1) NOT NULL,
[FromUserId] int NOT NULL,
[ToUserId] int NOT NULL,
[Content] nvarchar(1000) NOT NULL,
[AddTime] datetime NOT NULL,
[IsRead] bit NOT NULL
);
GO
-- --------------------------------------------------
-- Creating all PRIMARY KEY constraints
-- --------------------------------------------------
-- Creating primary key on [Id] in table 'UserSet'
ALTER TABLE [dbo].[UserSet]
ADD CONSTRAINT [PK_UserSet]
PRIMARY KEY CLUSTERED ([Id] ASC);
GO
-- Creating primary key on [Id] in table 'FriendReleateSet'
ALTER TABLE [dbo].[FriendReleateSet]
ADD CONSTRAINT [PK_FriendReleateSet]
PRIMARY KEY CLUSTERED ([Id] ASC);
GO
-- Creating primary key on [Id] in table 'MessageSet'
ALTER TABLE [dbo].[MessageSet]
ADD CONSTRAINT [PK_MessageSet]
PRIMARY KEY CLUSTERED ([Id] ASC);
GO
-- --------------------------------------------------
-- Creating all FOREIGN KEY constraints
-- --------------------------------------------------
-- --------------------------------------------------
-- Script has ended
-- --------------------------------------------------Code language: PHP (php)
16-说一下C/S系统的注册方式
CS系统的注册方式和BS系统有点差别,我们知道BS之间通讯是可以通过HTTP当CS其实也是可以通过HTTP的 不过可以不用。因为我们的CS 其实 如果是内部系统 你完全可以之间在代码访问数据据。通过ADO.NET直接访问数据库。但是这样 对于一些网络软件是不安全的。比如说 如果是直接客户端访问数据库 我们如果可以对字符串链接 进行加密界面 那么还好 这样不至于让人破解 得到数据库链接 这样得到了的话 是很危险的。
我们可以不让它直接访问数据库 。我们也是通过Socket协议 让客户端和服务器端 进行通讯 这样避免直接碰面。当然 如果更正式一点 这里的socket通讯 应该也要使用加密的方式 避免 其他的程序去和我们的QQ服务器通讯。
就是软件的设计 没有固定方式 我们的程序语法给了你 怎什么实现 那就看您了。
17-QQ服务器端开启Socket监听
我们的QQ服务器端只需要开启一个端口来监听所有的信息
我们整个服务器端 只有一个端口来接收全部信息。
那么就需要不同的信息 有不同的标记了。这样才能进行不同的处理。
我们看一下 我们大概有拿些信息
用户打开登录框,如果连不上 那么我们应该弹出一个网络无法访问的对话框。
还有注册申请消息
还有登录请求消息
还有聊天消息
还有好友请求的消息
class Program
{
static int port = 8885;
static Socket receiveSocket;
static void Main(string[] args)
{
//开启监听Socket
receiveSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
receiveSocket.Bind(new IPEndPoint(IPAddress.Any, port));
receiveSocket.Listen(20);//设置最多20个用户排队请求
Console.WriteLine("QQ服务以开启");
//接收一条消息
Socket socket = receiveSocket.Accept();
byte[] receiveByte = new byte[2048];
socket.Receive(receiveByte);
Console.WriteLine("接受到消息:"+Encoding.ASCII.GetString(receiveByte));
//退出QQ服务器
Console.WriteLine("关闭服务按N键回车");
string key = Console.ReadLine();
while (key!="N")
{
Console.WriteLine("关闭服务按N键回车");
key = Console.ReadLine();
}
}
}Code language: JavaScript (javascript)
18-QQ客户端网络不可用设置
当网络连不上QQ服务器端的时候,就弹出一个对话框。告诉用户当前网络不可以用。我们再App.xaml.cs里面去操作。
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
if (true)
{
MessageBox.Show("当前环境网络不可用。");
this.Shutdown();
}
}
}
19-QQ客户端登录后发起socket请求
当我们打开QQ客户端的时候 发起一个socket请求 如果没有收到回来的消息 则表示我们的网络有问题了 这个时候 不要弹出一个对话框 点确定以后 就关闭qq登录框
客户端发起socket请求的时候 需要知道服务器端的IP和端口号 但是不需要知道客户端的IP
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
//需要知道QQ服务器端的IP地址和端口号
string qqServiceIp = "127.0.0.1";
int port = 8885;
//准备socket
Socket clientSocket;
private void Application_Startup(object sender, StartupEventArgs e)
{
IPAddress ip = IPAddress.Parse(qqServiceIp);
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint endPoint = new IPEndPoint(ip, port);
try
{
clientSocket.Connect(endPoint);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
this.Shutdown();
}
//连得上则发送一条消息给qq服务器端
byte[] message = Encoding.Unicode.GetBytes("请求QQ服务器端");
clientSocket.Send(message);
}
}Code language: HTML, XML (xml)
20-循环接收QQ客户端的请求
我们修改服务器端 让我们的请求是循环请求的。 打开多个qq客户端
static void Main(string[] args)
{
//开启监听Socket
receiveSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
receiveSocket.Bind(new IPEndPoint(IPAddress.Any, port));
receiveSocket.Listen(20);//设置最多20个用户排队请求
Console.WriteLine("QQ服务以开启");
while (true)
{
//接收一条消息
Socket socket = receiveSocket.Accept();
byte[] receiveByte = new byte[2048];
socket.Receive(receiveByte);
string message = Encoding.Unicode.GetString(receiveByte).Trim();
Console.WriteLine("接受到消息:" + message.Replace("\0",""));
}
}Code language: JavaScript (javascript)
21-服务器端接收QQ客户端消息并回复
服务器端接收到客户端的消息后,需要给客户端回复一条消息,告诉客户端连接成功。
// 在接收消息后添加回复
byte[] sendByte = Encoding.Unicode.GetBytes("QQ服务器已连接成功");
socket.Send(sendByte);Code language: JavaScript (javascript)
22-客户端接收服务器端的回复消息
客户端发送消息后,需要接收服务器端的回复,以确认连接正常。
byte[] receiveByte = new byte[2048];
clientSocket.Receive(receiveByte);
string receiveMessage = Encoding.Unicode.GetString(receiveByte).Trim();
MessageBox.Show(receiveMessage);Code language: JavaScript (javascript)
23-使用JSON序列化消息对象
我们使用JSON来序列化消息对象,方便在客户端和服务器端之间传输。
public class MessageObj
{
public int Type { get; set; }
public string Content { get; set; }
public DateTime SendTime { get; set; }
}
// 序列化
MessageObj msg = new MessageObj();
msg.Type = 1;
msg.Content = "登录请求";
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);Code language: JavaScript (javascript)
24-消息类型的定义和区分
通过type标记来区分不同的消息类型:
1:注册请求
2:登录请求
3:添加好友请求
4:聊天消息
5:好友申请响应
25-客户端发送注册请求
客户端收集用户输入的注册信息,序列化为JSON后通过Socket发送。
// 注册消息
MessageObj msg = new MessageObj();
msg.Type = 1;
msg.Content = JsonConvert.SerializeObject(new { Niname = "测试用户", Password = "123456" });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
26-服务器端处理注册请求
服务器端接收注册请求,解析JSON,将用户信息存入数据库,并生成QQ号(从10001开始)。
// 解析注册消息
MessageObj msg = JsonConvert.DeserializeObject<MessageObj>(message);
if (msg.Type == 1)
{
var regInfo = JsonConvert.DeserializeObject<dynamic>(msg.Content);
// 存入数据库
UserSet user = new UserSet();
user.Niname = regInfo.Niname;
user.Password = regInfo.Password;
user.Mumber = (10000 + db.UserSet.Count() + 1).ToString();
user.AddTime = DateTime.Now;
db.UserSet.Add(user);
db.SaveChanges();
// 回复QQ号
byte[] reply = Encoding.Unicode.GetBytes("注册成功,您的QQ号为:" + user.Mumber);
socket.Send(reply);
}Code language: JavaScript (javascript)
27-客户端发送登录请求
客户端输入QQ号和密码,发送登录请求到服务器。
MessageObj msg = new MessageObj();
msg.Type = 2;
msg.Content = JsonConvert.SerializeObject(new { Mumber = "10001", Password = "123456" });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
28-服务器端处理登录请求
服务器端验证QQ号和密码,返回登录结果。
if (msg.Type == 2)
{
var loginInfo = JsonConvert.DeserializeObject<dynamic>(msg.Content);
var user = db.UserSet.FirstOrDefault(u => u.Mumber == loginInfo.Mumber && u.Password == loginInfo.Password);
if (user != null)
{
socket.Send(Encoding.Unicode.GetBytes("登录成功"));
}
else
{
socket.Send(Encoding.Unicode.GetBytes("QQ号或密码错误"));
}
}Code language: JavaScript (javascript)
29-客户端发送添加好友请求
客户端输入对方QQ号,发送添加好友请求。
MessageObj msg = new MessageObj();
msg.Type = 3;
msg.Content = JsonConvert.SerializeObject(new { FromUserId = "10001", ToUserId = "10002" });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
30-服务器端处理添加好友请求
服务器端记录好友申请到FriendReleate表,状态为未通过。
if (msg.Type == 3)
{
var friendInfo = JsonConvert.DeserializeObject<dynamic>(msg.Content);
FriendReleateSet fr = new FriendReleateSet();
fr.FromUserId = int.Parse(friendInfo.FromUserId.ToString());
fr.ToUserId = int.Parse(friendInfo.ToUserId.ToString());
fr.IsPass = false;
fr.AddTime = DateTime.Now;
db.FriendReleateSet.Add(fr);
db.SaveChanges();
socket.Send(Encoding.Unicode.GetBytes("好友申请已发送"));
}Code language: JavaScript (javascript)
31-客户端发送聊天消息
客户端选择好友,输入聊天内容,发送聊天消息。
MessageObj msg = new MessageObj();
msg.Type = 4;
msg.Content = JsonConvert.SerializeObject(new { FromUserId = "10001", ToUserId = "10002", Content = "你好,在吗?" });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
32-服务器端处理聊天消息并转发
服务器端接收聊天消息,存入Message表,并转发给目标用户(如果在线)。
if (msg.Type == 4)
{
var chatInfo = JsonConvert.DeserializeObject<dynamic>(msg.Content);
MessageSet messageSet = new MessageSet();
messageSet.FromUserId = int.Parse(chatInfo.FromUserId.ToString());
messageSet.ToUserId = int.Parse(chatInfo.ToUserId.ToString());
messageSet.Content = chatInfo.Content;
messageSet.AddTime = DateTime.Now;
messageSet.IsRead = false;
db.MessageSet.Add(messageSet);
db.SaveChanges();
// 转发给目标客户端(需要实现在线用户管理)
socket.Send(Encoding.Unicode.GetBytes("消息已发送"));
}Code language: JavaScript (javascript)
33-多客户端在线用户管理
服务器端需要维护一个在线用户列表,用于消息转发。
public static Dictionary<int, Socket> OnlineUsers = new Dictionary<int, Socket>();
// 登录成功后记录
OnlineUsers.Add(userId, socket);Code language: PHP (php)
34-好友申请列表的获取
客户端登录后,从服务器获取待处理的好友申请列表。
MessageObj msg = new MessageObj();
msg.Type = 5;
msg.Content = "10002"; // 当前用户QQ号
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
35-服务器端返回好友申请列表
服务器端查询FriendReleate表中IsPass=false且ToUserId为当前用户的记录。
if (msg.Type == 5)
{
int userId = int.Parse(msg.Content);
var requests = db.FriendReleateSet.Where(f => f.ToUserId == userId && f.IsPass == false).ToList();
string jsonResult = JsonConvert.SerializeObject(requests);
socket.Send(Encoding.Unicode.GetBytes(jsonResult));
}Code language: JavaScript (javascript)
36-处理好友申请同意/拒绝
客户端点击同意或拒绝按钮,发送处理结果到服务器。
MessageObj msg = new MessageObj();
msg.Type = 6;
msg.Content = JsonConvert.SerializeObject(new { RequestId = 1, IsAgree = true });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
37-服务器端更新好友关系
服务器端根据处理结果更新FriendReleate表的IsPass字段。
if (msg.Type == 6)
{
var result = JsonConvert.DeserializeObject<dynamic>(msg.Content);
int requestId = int.Parse(result.RequestId.ToString());
bool isAgree = bool.Parse(result.IsAgree.ToString());
var fr = db.FriendReleateSet.Find(requestId);
if (fr != null)
{
fr.IsPass = isAgree;
db.SaveChanges();
}
socket.Send(Encoding.Unicode.GetBytes(isAgree ? "已添加为好友" : "已拒绝申请"));
}Code language: JavaScript (javascript)
38-获取好友列表
客户端登录后获取好友列表,用于显示在主窗体。
MessageObj msg = new MessageObj();
msg.Type = 7;
msg.Content = "10001"; // 当前用户QQ号
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
39-服务器端返回好友列表
服务器端查询FriendReleate表中IsPass=true且涉及当前用户的记录,关联User表获取好友信息。
if (msg.Type == 7)
{
int userId = int.Parse(msg.Content);
var friends = db.FriendReleateSet
.Where(f => f.IsPass == true && (f.FromUserId == userId || f.ToUserId == userId))
.Select(f => new {
FriendId = f.FromUserId == userId ? f.ToUserId : f.FromUserId
})
.ToList();
// 获取好友详细信息
var friendDetails = new List<object>();
foreach (var f in friends)
{
var user = db.UserSet.Find(f.FriendId);
if (user != null)
{
friendDetails.Add(new { user.Mumber, user.Niname, user.Signal });
}
}
string jsonResult = JsonConvert.SerializeObject(friendDetails);
socket.Send(Encoding.Unicode.GetBytes(jsonResult));
}Code language: JavaScript (javascript)
40-离线消息的处理
当用户不在线时,消息存入数据库。用户上线后自动获取离线消息。
// 用户登录成功后检查离线消息
MessageObj msg = new MessageObj();
msg.Type = 8;
msg.Content = "10001"; // 当前用户QQ号
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
41-服务器端返回离线消息
查询Message表中IsRead=false且ToUserId为当前用户的记录。
if (msg.Type == 8)
{
int userId = int.Parse(msg.Content);
var offlineMessages = db.MessageSet.Where(m => m.ToUserId == userId && m.IsRead == false).ToList();
string jsonResult = JsonConvert.SerializeObject(offlineMessages);
socket.Send(Encoding.Unicode.GetBytes(jsonResult));
// 标记为已读
foreach (var om in offlineMessages)
{
om.IsRead = true;
}
db.SaveChanges();
}Code language: JavaScript (javascript)
42-聊天记录的查看
客户端可以选择好友查看聊天记录。
MessageObj msg = new MessageObj();
msg.Type = 9;
msg.Content = JsonConvert.SerializeObject(new { UserId = "10001", FriendId = "10002" });
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);Code language: JavaScript (javascript)
43-服务器端返回聊天记录
查询Message表中FromUserId和ToUserId匹配的记录,按时间排序。
if (msg.Type == 9)
{
var chatInfo = JsonConvert.DeserializeObject<dynamic>(msg.Content);
int userId = int.Parse(chatInfo.UserId.ToString());
int friendId = int.Parse(chatInfo.FriendId.ToString());
var chatHistory = db.MessageSet
.Where(m => (m.FromUserId == userId && m.ToUserId == friendId) || (m.FromUserId == friendId && m.ToUserId == userId))
.OrderBy(m => m.AddTime)
.ToList();
string jsonResult = JsonConvert.SerializeObject(chatHistory);
socket.Send(Encoding.Unicode.GetBytes(jsonResult));
}Code language: JavaScript (javascript)
44-心跳包检测客户端在线状态
客户端定时发送心跳包,服务器检测超时则移除在线用户。
// 客户端定时发送
System.Timers.Timer heartTimer = new System.Timers.Timer(30000);
heartTimer.Elapsed += (s, e) =>
{
MessageObj msg = new MessageObj();
msg.Type = 10;
msg.Content = "10001";
msg.SendTime = DateTime.Now;
string json = JsonConvert.SerializeObject(msg);
byte[] sendByte = Encoding.Unicode.GetBytes(json);
clientSocket.Send(sendByte);
};
heartTimer.Start();Code language: JavaScript (javascript)
45-服务器端心跳包处理
服务器记录每个用户最后心跳时间,超时则断开连接。
public static Dictionary<int, DateTime> LastHeartTime = new Dictionary<int, DateTime>();
if (msg.Type == 10)
{
int userId = int.Parse(msg.Content);
LastHeartTime[userId] = DateTime.Now;
socket.Send(Encoding.Unicode.GetBytes("ok"));
}
// 定时检查超时
System.Timers.Timer checkTimer = new System.Timers.Timer(60000);
checkTimer.Elapsed += (s, e) =>
{
var timeoutUsers = LastHeartTime.Where(kv => (DateTime.Now - kv.Value).TotalSeconds > 60).Select(kv => kv.Key).ToList();
foreach (var uid in timeoutUsers)
{
OnlineUsers.Remove(uid);
LastHeartTime.Remove(uid);
}
};
checkTimer.Start();Code language: JavaScript (javascript)
46-项目总结和扩展思路
本项目基于.NET Winform/WPF、Socket、EF DataFirst、三层架构实现了仿QQ多用户聊天软件的核心功能。
包括:登录/注册/加好友/文本聊天,服务器端使用控制台程序+Socket监听端口8885,数据库使用EF DataFirst(UserSet/FriendReleateSet/MessageSet三表),消息通过json序列化+type标记区分。
扩展思路:可添加群聊功能、文件传输、表情包、消息加密、使用WCF或SignalR替代原生Socket等。