幸运星空

Lucker的程序人生

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

花一天的时间,根据老大的指示,用UDP做了一个局域网内聊天的程序。以前从没做过UDP通信方面的程序,只做过一些比较管理的SOCKET TCP通讯,所以刚开始的时候,还是有点不知从何下手的味道,但是后来从网上找了几个相关的例子临时抱佛脚的学习了一下,毕竟,局域网内聊天程序并不是什么高难道的项目,UDP也不是什么新鲜玩意儿。网上相关实例多如牛毛。终于有了点门道,就开始着手规划自己的聊天程序了。

这个程序的用途是用来让多个管理员同时为多位用户在线答疑的。根据老大的要求,要以服务程序为核心,所有通讯必须经过服务程序,服务程序常开,运行在服务器上。要能够显示当前在线用户,并且能实现示读消息缓存,下次用户登录时再送达。两类用户:管理员和普通用户,前者要能接收到所有消息,并能向指定普通用户和全部在线用户发送消息,而后者只能向管理员发送消息,并接收属于自己的消息或目标是全部用户的消息。还要能查看历史聊天记录。所有用户登录要经过保存在数据库里的注册用户的认证。

先贴图吧(本来想把UML图一起贴上来的,但后来发现程序逻辑和结构都是再简单不过的了,就省下时间来没去画了)

image
服务端界面

image
客户端界面

image
历史聊天记录界面

image
软件配置界面

以下是服务端实现的核心代码:
using System;
using System.Collections;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Lucker.LogManager;
//程序中使用到线程
using System.Text;
//程序中使用到编码

namespace UDPServer
{
    public partial class Server : Form
    {
        private LogManager lm = new LogManager();
        private UdpClient server;
        private IPEndPoint receivePoint;
        private int port = 8080;
        //定义端口号
        private int ip = 127001;
        //设定本地IP地址
        private Thread startServer;

        string[] temp;
        byte[] recData;
        Hashtable UserList = new Hashtable();
        Hashtable AdminList = new Hashtable();       
        Queue SendToAdmin = new Queue();
        Queue SendToUser = new Queue();
        //接收信息
        //接收数据的字符串格式为:IP地址|端口号|用户名|是否管理员(Y/N)|接收用户名|信息内容
        public void start_server()
        {
            MethodInvoker mi = new MethodInvoker(ThreadFun);
            while (true)
            {
                //接收从远程主机发送到本地8080端口的数据
                recData = server.Receive(ref receivePoint);
                ASCIIEncoding encode = new ASCIIEncoding();
                //获得客户端请求数据
                string Read_str = encode.GetString(recData);
                //提取客户端的信息,存放到定义为temp的字符串数组中
                temp = Read_str.Split("|".ToCharArray());
                //信息长度不对,放弃
                if (temp.Length != 6)
                {
                    lm.WriteFileLog("Error data format", Read_str);
                    continue;
                }
                AddtoList();
                switch (temp[5].Substring(0,temp[5].IndexOf("&E")))
                {
                    case "GetOnLineAdmin":
                        string[] admin;
                        string adminstring="";
                        admin = new string[AdminList.Count];
                        AdminList.Keys.CopyTo(admin, 0);
                        if (admin.Length != 0)
                        {
                            foreach (string s in admin)
                            {
                                adminstring += s + ",";
                            }                           
                        }
                        SendMsg("@:GetOnLineAdmin&E" + adminstring);
                        continue;
                    case "GetOnLineUser":
                        string[] user;
                        string userstring = "";
                        user = new string[UserList.Count];
                        UserList.Keys.CopyTo(user, 0);
                        if (user.Length != 0)
                        {
                            foreach (string s in user)
                            {
                                userstring += s + ",";
                            }                           
                        }
                        userstring += "All,";
                        SendMsg("@:GetOnLineUser&E" + userstring);
                        continue;
                    default:
                        break;
                }
                //显示信息
                BeginInvoke(mi);//让主线程去访问自己创建的控件.
            }
        }
        private void SendMsg(string msg)
        {
            msg = string.Format("{0}|{1}|{2}|{3}|{4}|", "", "", "System", "N",temp[2]) + msg+"&E";
            Byte[] buffer = null;
            Encoding ASCII = Encoding.ASCII;
            buffer = new Byte[msg.Length + 1];
            int len = ASCII.GetBytes(msg.ToCharArray(), 0, msg.Length, buffer, 0);
            server.Send(buffer, len, temp[0], Int32.Parse(temp[1]));
        }
        //处理异步调用
        private void ThreadFun()
        {           
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (recData != null && temp != null)
            {
                temp[5] = temp[5].Substring(0, temp[5].Length - 2);
                if (temp[4] == "")
                {
                    msg.AppendText(string.Format("{1}:\t[{0}] said\r\n\t{2}\r\n", temp[2], datetime, temp[5]));
                    lm.WriteFileLog(string.Format("[{0}]", temp[2]), temp[5]);
                }
                else
                {
                    msg.AppendText(string.Format("{1}:\t[{0}] said to [{3}]\r\n\t{2}\r\n", temp[2], datetime, temp[5], temp[4]));
                    lm.WriteFileLog(string.Format("[{0}] said to [{1}]", temp[2], temp[4]), temp[5]);
                }
                string[] ip = { temp[0], temp[1] };
                if (temp[3] == "Y")
                {
                    SendToUser.Enqueue(recData);
                    SendToAdmin.Enqueue(recData);//A 管理员的信息要能转发到B 管理员电脑上。
                }
                else
                {
                    SendToAdmin.Enqueue(recData);
                }

                recData = null;
                temp = null;
            }
        }
        //添加用户到列表中
        private void AddtoList()
        {           
            string[] ip = { temp[0], temp[1] };
            if (temp[3] == "Y")
            {
                if (!AdminList.ContainsKey(temp[2]))
                {
                    AdminList.Add(temp[2], ip);
                }
            }
            else
            {
                if (!UserList.ContainsKey(temp[2]))
                {
                    UserList.Add(temp[2], ip);
                }
            }
        }
        public Server()
        {
            InitializeComponent();
        }

        //启动服务
        private void Server_Load(object sender, EventArgs e)
        {
            timer1.Enabled = true;
            run();
        }
        public void run()
        {
            //利用本地8080端口号来初始化一个UDP网络服务
            server = new UdpClient(port);
            receivePoint = new IPEndPoint(new IPAddress(ip), port);
            //开一个线程
            startServer = new Thread(new ThreadStart(start_server));
            //启动线程
            startServer.Start();
            toolStripStatusLabel_state.Text = "UDP Chat Server is running";
        }

        //清除服务器端程序日志
        private void button1_Click(object sender, EventArgs e)
        {
            msg.Text = "";
        }

        //转发用户信息到每一位管理员电脑上:
        //转发管理员信息到指定的用户电脑上:
        private void timer1_Tick(object sender, EventArgs e)
        {
            if (SendToAdmin.Count > 0 && AdminList.Count > 0)
            {
                byte[] senddate;
                string[] data;
                for (int i = 0; i < SendToAdmin.Count; i++)
                {
                    ICollection keys = AdminList.Keys;
                    string[] value = new string[2];
                    senddate = (byte[])SendToAdmin.Dequeue();
                    ASCIIEncoding encode = new ASCIIEncoding();
                    //获得客户端请求数据
                    string Read_str = encode.GetString(senddate);
                    //提取客户端的信息,存放到定义为temp的字符串数组中
                    data = Read_str.Split("|".ToCharArray());
                    foreach (string admin in keys)
                    {
                        value = (string[])AdminList[admin];
                        if (data[2] == admin) continue;//自己发了的信息,不应再回发给自己了。
                        server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                        Thread.Sleep(100);
                    }
                }
            }
            if (SendToUser.Count > 0 && UserList.Count > 0)
            {
                byte[] senddate;
                string[] data;
                for (int i = 0; i < SendToUser.Count; i++)
                {
                    ICollection keys = UserList.Keys;
                    string[] value = new string[2];
                    senddate = (byte[])SendToUser.Dequeue();
                    ASCIIEncoding encode = new ASCIIEncoding();
                    //获得客户端请求数据
                    string Read_str = encode.GetString(senddate);
                    //提取客户端的信息,存放到定义为temp的字符串数组中
                    data = Read_str.Split("|".ToCharArray());
                    foreach (string user in keys)
                    {
                        if (data[4] == "All")//发给所有人。
                        {
                            value = (string[])UserList[user];
                            server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                            senddate = null;
                            Thread.Sleep(100);
                        }
                        else
                            if (user == data[4])//查找到接收用户,发送消息
                            {
                                value = (string[])UserList[user];
                                server.Send(senddate, senddate.Length, value[0], Int32.Parse(value[1]));
                                senddate = null;
                                Thread.Sleep(100);
                                break;
                            }
                    }
                    if (senddate != null)//消息没有发送出去,重新和队。
                    {
                        SendToUser.Enqueue(senddate);
                    }
                }
            }
            //日志太长,自动清空。
            if (msg.Text.Length > 32766)
            {
                msg.Text = "";
            }
            //更新在线列表,客户端有信息来时自动增加:
            for (int i = 0; i < admin_list.Items.Count;)
            {
                admin_list.Items.RemoveAt(i);
            }
            for (int i = 0; i < user_list.Items.Count;)
            {
                user_list.Items.RemoveAt(i);
            }
            ICollection keys1 = UserList.Keys;
            foreach (string user in keys1)
            {
                user_list.Items.Add(user);
            }
            ICollection keys2 = AdminList.Keys;
            foreach (string admin in keys2)
            {
                admin_list.Items.Add(admin);
            }
            UserList.Clear();
            AdminList.Clear();
        }

        private void tb_his_Click(object sender, EventArgs e)
        {
            msg His = new msg("History");
            His.Show();
        }

    }
}

以下是客户端实现的核心代码:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Lucker.LogManager;

namespace UDPClient
{
    public partial class Client : Form
    {
        Register reg = new Register();
        private LogManager lm = new LogManager();
        string User;
        string IsAdmin = "N";
        private static UdpClient m_Client;
        private static string m_szHostName;
        string local_IP;
        private static IPHostEntry m_LocalHost;
        private static IPEndPoint m_RemoteEP;
        private Thread th;
        string[] data;
        String strData;
        string Sendto = "";
        string connectionstring;
        static int RemotePort;
        static int LocalPort;
        private static IPAddress m_GroupAddress_S;//要发送到的计算机IP

        public Client()
        {
            InitializeComponent();
        }

        //登录
        private void button3_Click(object sender, EventArgs e)
        {
            if(button3.Text=="&Login")
            {
                SqlConnection con = new SqlConnection(connectionstring);
                SqlCommand com = new SqlCommand();
                com.Connection = con;
                com.CommandText = string.Format("select count(*) from [UserInfor] where [UserName]='{0}' and [Password]='{1}'", tb_user.Text.Trim(), tb_pwd.Text.Trim());
                try
                {
                    if (con.State != ConnectionState.Open) con.Open();
                    //ReceivedMsg.AppendText("[System]:\tConnected!\r\n");
                    if (int.Parse(com.ExecuteScalar().ToString()) != 1)//登录不成功
                    {
                        ReceivedMsg.AppendText("[System]:\tUser name or password is wrong.Please try again.\r\n");
                        return;
                    }
                    User = tb_user.Text.Trim();
                    //是否是管理员
                    com.CommandText = string.Format("select [Class] from [UserInfor] where [UserName]='{0}'", User);
                    object oIsAdmin=com.ExecuteScalar();
                    if (oIsAdmin != null)
                    {
                        if (Convert.ToInt32(oIsAdmin) == 0)
                        {
                            IsAdmin = "Y";
                            Sendto = "All";
                            button1.Text = "&Send to All";
                            ReceivedMsg.AppendText("[System]:\tAdmin <" + User + "> login.\r\n");
                        }
                        else
                        {
                            IsAdmin = "N";
                            ReceivedMsg.AppendText("[System]:\tUser <" + User + "> login.\r\n");
                        }
                    }
                    else
                    {
                        IsAdmin = "N";
                        ReceivedMsg.AppendText("[System]:\tUser <" + User + "> login.\r\n");
                    }
                    SendMsg("I login.");
                    Text = User + " is on line";
                    button1.Enabled = true;
                    tb_SendMsg.Focus();
                    button3.Text = "&Logout";
                    tb_user.Enabled = false;
                    tb_pwd.Enabled = false;
                    cbx_remb.Enabled = false;
                    //GetOnLine();
                    //Thread.Sleep(500);
                    timer1.Enabled = true;

                    //保存登录记录
                    if (cbx_remb.Checked)
                    {
                        string id = tb_user.Text.Trim();
                        string pwd = tb_pwd.Text.Trim();
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "Last user", id, Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "User pwd", pwd, Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "Remember", "Y", Microsoft.Win32.RegistryValueKind.String);
                    }
                    else
                    {
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "Last user", "", Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "User pwd", "", Microsoft.Win32.RegistryValueKind.String);
                        reg.WriteReg(@"Software\LuckerSoft\UDPChat\", "Remember", "N", Microsoft.Win32.RegistryValueKind.String);
                    }
                }
                catch (SqlException sqlex)
                {
                    ReceivedMsg.AppendText("[System]:\t"+sqlex.Message+"\r\n");
                }
            }
            else
            {
                Sendto = "All";
                SendMsg("I logout.");
                timer1.Enabled = false;
                button3.Text = "&Login";
                tb_user.Enabled = true;
                tb_pwd.Enabled = true;
                cbx_remb.Enabled = true;
                User = "";
                button1.Enabled = false;
                button1.Text = "&Send to";
                Sendto = "";
                Text = "Chat";
                for (int i = 0; i < AdminList.Items.Count;)
                {
                    AdminList.Items.RemoveAt(i);
                }
                for (int i = 0; i < UserList.Items.Count;)
                {
                    UserList.Items.RemoveAt(i);
                }
            }
        }

        //退出
        private void button2_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (AdminList.Items.Count == 0 && UserList.Items.Count == 0)
            {
                ReceivedMsg.AppendText("[System]:\tThere is no Admin or User online, your message may not be read!\r\n");
                return;
            }
            if (button1.Text == "&Send to" &&IsAdmin=="Y")
            {
                ReceivedMsg.AppendText("[System]:\tPlease choose a user to chat.\r\n");
                return;
            }
            string msg = tb_SendMsg.Text.Trim();
            if (msg.Length == 0) return;
            SendMsg(msg);
            tb_SendMsg.Text = "";
            tb_SendMsg.Focus();
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (IsAdmin == "Y")
            {
                ReceivedMsg.AppendText(string.Format("{1}:\t[{0}] said to [{3}]\r\n\t{2}\r\n", User, datetime, msg, Sendto));
                lm.WriteFileLog(string.Format("[{0}] said to [{1}]", User, Sendto), msg);
            }
            else
            {
                ReceivedMsg.AppendText(string.Format("{1}:\t[{0}] said\r\n\t{2}\r\n", User, datetime, msg));
                lm.WriteFileLog(string.Format("[{0}] said", User), msg);
            }
        }
        private void Client_Load(object sender, EventArgs e)
        {           
            string ip= (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "IP", "127.0.0.1", true, Microsoft.Win32.RegistryValueKind.String);
            string port = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "Port", "8080", true, Microsoft.Win32.RegistryValueKind.String);
            string server = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "Server", "192.168.3.19", true, Microsoft.Win32.RegistryValueKind.String);
            string db = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "DataBase", "Stock", true, Microsoft.Win32.RegistryValueKind.String);
            string id = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "UserID", "sa", true, Microsoft.Win32.RegistryValueKind.String);
            string pwd = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "Passwork", "ati1234", true, Microsoft.Win32.RegistryValueKind.String);
            connectionstring=string.Format("Data Source={0};Initial Catalog={1};User ID={2};Password={3};",server,db,id,pwd);

            tb_user.Text = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "Last user", "", true, Microsoft.Win32.RegistryValueKind.String);
            tb_pwd.Text = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "User pwd", "", true, Microsoft.Win32.RegistryValueKind.String);
            string remb = (string)reg.ReadReg(@"Software\LuckerSoft\UDPChat\", "Remember", "N", true, Microsoft.Win32.RegistryValueKind.String);
            if (remb == "Y")
                cbx_remb.Checked = true;
            else
                cbx_remb.Checked = false;

            RemotePort = int.Parse(port);
            LocalPort = RemotePort;
            m_GroupAddress_S = IPAddress.Parse(ip);//要发送到的计算机IP

            System.Net.IPHostEntry localhost = Dns.GetHostEntry(Dns.GetHostName());
            local_IP = localhost.AddressList[0].ToString();//接收消息的本地IP,用于监听
            //m_Address_C = IPAddress.Parse(local_IP);

            m_szHostName = Dns.GetHostName();
            m_LocalHost = Dns.GetHostEntry(m_szHostName);
            //实例化UdpCLient 
            m_Client = new UdpClient(LocalPort);
            //创建对方主机的终结点 
            m_RemoteEP = new IPEndPoint(m_GroupAddress_S, RemotePort);

            th = new Thread(new ThreadStart(Listener));
            th.Start();
        }

        public void Listener()
        {
            MethodInvoker mi = new MethodInvoker(ThreadFun);
            Encoding ASCII = Encoding.ASCII;
            while (true)
            {
                Byte[] recdata = m_Client.Receive(ref m_RemoteEP);
                strData = ASCII.GetString(recdata);
                //提取信息,存放到定义为data的字符串数组中
                data = strData.Split("|".ToCharArray());
                //信息长度不对,放弃
                if (data.Length != 6)
                {
                    lm.WriteFileLog("Error data format", strData);
                    continue;
                }
                //显示信息
                BeginInvoke(mi);//让主线程去访问自己创建的控件.
            }
        }

        private void ThreadFun()
        {
            string datetime = string.Format("{0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
            if (strData != null && data != null)
            {
                switch (data[5].Substring(0,data[5].IndexOf("&E")))
                {
                    case "GetOnLineAdmin"://客户端不响应获取管理员列表的请求
                    case "GetOnLineUser":
                        break;
                    case "@:GetOnLineAdmin"://管理员列表信息 "@:"表示响应请求.
                        string admins=data[5].Substring(data[5].IndexOf("&E")+2);
                        if (admins.Length > 3)
                        {
                            admins = admins.Substring(0, admins.Length - 3);
                            string[] adminlist = admins.Split(",".ToCharArray());
                            for (int i = 0; i < AdminList.Items.Count;)
                                AdminList.Items.RemoveAt(i);
                            AdminList.Items.AddRange(adminlist);
                        }
                        break;
                    case "@:GetOnLineUser"://管理员列表信息 "@:"表示响应请求.
                        string users = data[5].Substring(data[5].IndexOf("&E") + 2);
                        if (users.Length > 3)
                        {
                            users = users.Substring(0, users.Length - 3);
                            string[] userlist = users.Split(",".ToCharArray());
                            for (int i = 0; i < UserList.Items.Count; )
                                UserList.Items.RemoveAt(i);
                            UserList.Items.AddRange(userlist);
                        }
                        break;
                    default://普通会话
                        data[5] = data[5].Substring(0, data[5].Length - 2);
                        if (data[4] == "")
                        {
                            ReceivedMsg.AppendText(string.Format("{1}:\t[{0}] said\r\n\t{2}\r\n", data[2], datetime, data[5]));
                            lm.WriteFileLog(string.Format("[{0}]", data[2]), data[5]);
                        }
                        else
                        {
                            ReceivedMsg.AppendText(string.Format("{1}:\t[{0}] said to [{3}]\r\n\t{2}\r\n", data[2], datetime, data[5], data[4]));
                            lm.WriteFileLog(string.Format("[{0}] said to [{1}]", data[2], data[4]), data[5]);
                        }
                        break;
                }
                strData = null;
                data = null;
            }
        }
        private void SendMsg(string msg)
        {
            msg = string.Format("{0}|{1}|{2}|{3}|{4}|", local_IP, LocalPort, User, IsAdmin, Sendto) + msg + "&E";
            Byte[] buffer = null;
            Encoding ASCII = Encoding.ASCII;
            buffer = new Byte[msg.Length + 1];
            int len = ASCII.GetBytes(msg.ToCharArray(), 0, msg.Length, buffer, 0);
            m_Client.Send(buffer, len, m_RemoteEP);
        }
        //获得在线管理员列表:
        private void GetOnLine()
        {
            SendMsg("GetOnLineAdmin");
            Thread.Sleep(100);
            SendMsg("GetOnLineUser");
        }

        private void timer1_Tick(object sender, EventArgs e)
        {
            GetOnLine();
        }

        private void UserList_MouseClick(object sender, MouseEventArgs e)
        {
            if (IsAdmin == "N") return;
            if (UserList.SelectedItems.Count == 0)
            {
                //ReceivedMsg.AppendText("[System]:\tPlease choose a user to chat.\r\n");
                return;
            }
            Sendto = UserList.SelectedItem.ToString();
            button1.Text = "&Send to " + Sendto;
        }

        private void history_Click(object sender, EventArgs e)
        {
            msg His = new msg("History");
            His.Show();
        }

        private void Config_Click(object sender, EventArgs e)
        {
            Config config = new Config();
            config.ShowDialog();
        }
    }
}

贴完代码,实现自己都感觉代码很乱,只能够说把功能实现了,设计模式,编程思想什么的就没去考虑了。希望大家多给我提些改进的意见或建议,逐步完善这个程序。

有需要源码的朋友请继续关注本文,我会再过几天将源码奉上,在文章结尾处贴出下载链接。(链接已经放出,下载及注意事项请看本人的下一篇补充博文)。

后语

程序做完了,却始终没有弄明白这个程序为什么老大非要我用UDP实现,我想:同样的功能,用TCP的话要简单多了。UDP最大的特点就是无连接通讯,实现简单。而本例中UDP无连接的特性恰恰是确实用户在线状态的最大的不方便之处,在TCP程序中,对方的上线和下线,哪怕里网络中断都可以即时反映出来,无需太多的代码就可以实现,虽然本例中通过客户端不断的向服务器轮询基本上确保了在可以接受的短时间内获得在线用户列表,但实现它的代价是十分昂贵的,不仅支出了相当大量的网络流量(绝大部分通讯流量是轮询)和系统时间,相比TCP来说,实在不合算。如果硬要用UDP做通信的话,其实,之前还有一个相对“经济”一点的方案,那就是:客户端打开后,通过UDP广播获得服务端的IP地址和端口,从而和服务端取得联系。每个客户端运行后都在服务端保存它的IP地址和端口,这样,当一个终端要和另一个终端通讯时,可以从服务端获得对方的IP地址和端口,然后就可以实现两个终端之间的直接通信。服务端只在其中扮演“查询字典”的角色。这个方案和上面实现的方案各有特色,读者朋友们可以自己研究研究。

不知不觉,已近午夜。呵呵,该睡觉了。

posted on 2009-06-10 23:56  Lucker  阅读(9042)  评论(10编辑  收藏  举报