.NET 开发的访QQ聊天工具

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等。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注