即时通信聊天工具的原理与设计

转载:http://www.cnblogs.com/phquan/archive/2012/03/26/2417460.html

该软件采用P2P方式,各个客户端之间直接发消息进行会话聊天,服务器在其中只扮演协调者的角色(混合型P2P)。

1.会话流程设计

   当一个新用户通过自己的客户端登陆系统后,从服务器获取当前在线的用户信息列表,列表信息包括了系统中每个用户的地址。用户就可以开始独立工作,自主地向其他用户发送消息,而不经过服务器。每当有新用户加入或在线用户退出时,服务器都会及时发消息通知系统中的所有其他用户,以便它们实时地更新用户信息列表。

  按照上述思路,设计系统会话流程如下:

  (1)用户通过客户端进入系统,向服务器发出消息,请求登陆。

  (2)服务器收到请求后,向客户端返回应答消息,表示同意接受该用户加入,并顺带将自己服务线程所在的监听端口号告诉用户。

  (3)客户端按照服务器应答中给出的端口号与服务器建立稳定的连接。

  (4)服务器通过该连接将当前在线用户的列表信息传给新加入的客户端。

  (5)客户端获得了在线用户列表,就可以独立自主地与在线的其他用户通信了。

  (6)当用户退出系统时要及时地通知服务器。

2.用户管理

系统中,无论是服务器还是客户端都保存一份在线用户列表,客户端的用户表在一开始登陆时从服务器索取获得。在程序运行的过程中,服务器负责实时地将系统内用户的变动情况及时地通知在线的每个成员用户。

新用户登录时,服务器将用户表传给他,同时向系统内每个成员广播“login”消息,各成员收到后更新自己的用户表。

同样,在有用户退出系统时,服务器也会及时地将这一消息传给各个用户,当然这也就要求每个用户在自己想要退出之前,必须要先告诉服务器。

3.协议设计

3.1 客户端与服务器会话

(1)登陆过程。

  客户端用匿名UDP向服务器发送消息:

  login,username,localIPEndPoint

  消息内容包括3个字段,各字段之间用“,”分隔:“login”表示请求登陆;“username”为用户名;“localIPEndPoint”是客户端本地地址。

  服务器收到后以匿名UDP返回如下消息:

  Accept,port

  其中,“Accept”表示服务器接受了请求;“port”是服务所在端口,服务线程在这个端口上监听可能的客户连接,该连接使用同步的TCP。

  连上服务器,获取用户列表:

  客户端从上一会话的“port”字段的值服务所在端口,于是向端口发起TCP连接,向服务器索取在线的用户列表,服务器接受连接后将用户列别传输给客户端。

  用户列表格式如下:

  username1,IPEndPoint1;username2,IPEndPoint2;.....;end

  username1,username2.....为用户名,IPEndPoint1,IPEndPoint2....为它们对应的端点。每个用户的信息都有个“用户名+端点”组成,用户信息之间以“;”隔开,整个用户列表以“end”结尾。

3.2 服务器协调管理用户

(1)新用户加入通知。

  由于系统中已存在的每个用户都有一份当前用户表,因此当有新成员加入时,服务器无需重复给系统中的每个成员再传送用户表,只要将新加入成员的信息告诉系统内的其他用户,再由他们各自更新自己的用户表就行了。

  服务器向系统内用户广播发送如下消息:

  端点字段写为“remoteIPEndPoint”,表示是远程某个用户终端登陆了,本地客户线程据此更新用户列表。其实,在这个过程中,服务器只是将受到的“login”消息简单地转发而已。

 (2)用户退出。

  与新成员加入时一样,服务器将用户退出的消息直接进行广播转发:

  logout,username,remoteIPEndPoint

  其中,“remoteIPEndPoint”为退出系统的远程用户终端的端点地址。

3.3 用户终端之间聊天

  用户聊天时,他们各自的客户端之间是以P2P方式工作的,彼此地位对等,独立,不与服务器发生直接联系。

  聊天时发送的信息格式为:

  talk,longTime,selfUserName,message

  “talk”表明这是聊天内容;“longTime”是长时间格式的当前系统时间;“selfUserName”为自己的用户名;“message”是聊天的内容。

4.系统实现

4.1 服务线程

系统运行后,先有服务器启动服务线程,只需单击“启动”按钮即可。

“启动”按钮的事件过程:
//点击开始事件处理函数
        private void buttonStart_Click(object sender, EventArgs e)
        {
            //创建接收套接字
            serverIp = IPAddress.Parse(textBoxServerIp.Text);
            serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
            receiveUdpClient = new UdpClient(serverIPEndPoint);

            //启动接收线程
            Thread threadReceive = new Thread(ReceiveMessage);
            threadReceive.Start();
            buttonStart.Enabled = false;
            buttonStop.Enabled = true;

            //随机指定监听端口 N( P+1 ≤ N < 65536 )
            Random random = new Random();
            tcport = random.Next(port + 1, 65536);

            //创建监听套接字
            myTcpListener = new TcpListener(serverIp, tcport);
            myTcpListener.Start();

            //启动监听线程
            Thread threadListen = new Thread(ListenClientConnect);
            threadListen.Start();
            AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
        }

可以看到,服务器先后启动了两个线程:一个是接收线程threadReceive,它在一个实名UDP端口上,时刻准备着接收客户端发来的会话消息;另一个是监听线程threadListen,它在某个随机指定的端口上监听。

  服务器接收线程关联的ReceiveMessage()方法:
//接收数据
        private void ReceiveMessage()
        {
            IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
            while (true)
            {
                try
                {
                    //关闭receiveUdpClient时此句会产生异常
                    byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
                    string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);

                    //显示消息内容
                    AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));

                    //处理消息数据
                    string[] splitString = message.Split(',');

                    //解析用户端地址
                    string[] splitSubString = splitString[2].Split(':');   //除去':'
                    IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
                    switch (splitString[0])
                    {
                        //收到注册关键字"login"
                        case "login":
                            User user = new User(splitString[1], clientIPEndPoint);
                            userList.Add(user);
                            AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
                            string sendString = "Accept," + tcport.ToString();
                            SendtoClient(user, sendString);   //向该用户发送同意关键字
                            AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
                            for (int i = 0; i < userList.Count; i++)
                            {
                                if (userList[i].GetName() != user.GetName())
                                {
                                    //向除刚加入的所有用户发送更新消息
                                    SendtoClient(userList[i], message);
                                }
                            }
                            AddItemToListBox(string.Format("广播:[{0}]", message));
                            break;

                        //收到关键字"logout"
                        case "logout":
                            for (int i = 0; i < userList.Count; i++)
                            {
                                if (userList[i].GetName() == splitString[1])
                                {
                                    AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
                                    userList.RemoveAt(i);
                                }
                            }

                            //向所用用户发送更新消息
                            for (int i = 0; i < userList.Count; i++)
                            {
                                SendtoClient(userList[i], message);
                            }
                            AddItemToListBox(string.Format("广播:[{0}]", message));
                            break;
                    }
                }
                catch
                {
                    break;
                }
            }
            AddItemToListBox(string.Format("服务线程({0})终止", serverIPEndPoint));
        }

接收线程执行该方法,进入while()循环,对每个收到的消息进行解析,根据消息头是“login”或“logout”转入相应的处理。

 监听线程对应ListenClientConnect()方法:
//接受客户端连接
        private void ListenClientConnect()
        {
            TcpClient newClient = null;
            while (true)
            {
                try
                {
                    //获得用于传递数据的TCP套接口
                    newClient = myTcpListener.AcceptTcpClient();
                    AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
                }
                catch
                {
                    AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
                    break;
                }

                //启动发送用户列表线程
                Thread threadSend = new Thread(SendData);
                threadSend.Start(newClient);
            }
        }

当客户端请求到达后,与之建立TCP连接,然后创建一个新的线程threadSend,他通过执行SendData()方法传送用户列表。

  在服务器运行过程中,可随时通过点击“停止”按钮关闭服务线程。

  ”停止“按钮的事件过程:
//当点击关闭按钮的事件处理程序
        private void buttonStop_Click(object sender, EventArgs e)
        {
            myTcpListener.Stop();
            receiveUdpClient.Close();
            buttonStart.Enabled = true;
            buttonStop.Enabled = false;
        }

这里myTcpListener是TCP监听套接字,而receiveUdpClient是UDP套接字。当执行Stop()方法关闭监听套接字时,myTcpListener.AcceptTcpClient()会产生异常。
《即时通信聊天工具的原理与设计》

4.2 登陆/注销

(1) 用户对象

 为了便于服务器对全体用户的管理,在服务器工程中添加自定义User类。代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//添加的命名空间引用
using System.Net;

namespace Server
{
    //用户信息类 蒲泓全(18/3/2012)
    class User
    {
        private string userName;           //用户名
        private IPEndPoint userIPEndPoint;  //用户地址 
        public User(string name, IPEndPoint ipEndPoint)
        {
            userName = name;
            userIPEndPoint = ipEndPoint;
        }
        public string GetName()
        {
            return userName;
        }
        public IPEndPoint GetIPEndPoint()
        {
            return userIPEndPoint;
        }
    }
}

User类具有用户名和端点地址两个属性,这也正是用户列表中需要填写的信息项。
(2) 用户登录功能

  当服务器的两个服务线程运行起来之后,各用户就可以通过客户端程序登录到系统了。用户在客户端上单击“登录”按钮后,客户端就向服务器发出“login”请求。

 “登录”按钮的事件过程为:
private void buttonLogin_Click(object sender, EventArgs e)
        {
            //创建接收套接字
            IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
            clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
            receiveUdpClient = new UdpClient(clientIPEndPoint);

            //启动接收线程
            Thread threadReceive = new Thread(ReceiveMessage);
            threadReceive.Start();
            AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));

            //匿名发送
            sendUdpClient = new UdpClient(0);

            //启动发送线程
            Thread threadSend = new Thread(SendMessage);
            threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
            AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
            buttonLogin.Enabled = false;
            buttonLogout.Enabled = true;
            this.Text = textBoxUserName.Text;  //使当前窗体名字变为当前用户名
        }

可以看到客户端在登录时也启动了两个线程,其中一个threadReceive是用实名UDP创建的接收线程,又称为客户线程,这是因为,它代表客户端程序处理与服务器的会话消息。另一个线程threadSend则是临时创建的,并以匿名UDP向服务器发出“login”消息。
登陆请求发出之后,客户线程就循环执行ReceiveMessage()方法,以随时接受和处理服务器的应答消息。
客户线程关联的ReceiveMessage()方法:

//接收数据
        private void ReceiveMessage()
        {
            IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
            while (true)
            {
                try
                {
                    //关闭receiveUdpClient时此句会产生异常
                    byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
                    string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);

                    //显示消息内容
                    AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));

                    //处理消息数据
                    string[] splitString = message.Split(',');   //除去','
                    switch (splitString[0])
                    {
                        //若接收连接
                        case "Accept":
                            try
                            {
                                AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
                                myTcpClient = new TcpClient();
                                myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
                                if (myTcpClient != null)
                                {
                                    AddItemToListBox("连接成功!");
                                    networkStream = myTcpClient.GetStream();
                                    br = new BinaryReader(networkStream);
                                }
                            }
                            catch
                            {
                                AddItemToListBox("连接失败!");
                            }
                            Thread threadGetList = new Thread(GetUserList);
                            threadGetList.Start(); //请求获得用户列表信息线程启动
                            break;

                            //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
                        case "login":
                            AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
                            string userItemInfo = splitString[1] + "," + splitString[2];
                            AddItemToListView(userItemInfo);
                            break;

                        //若收到注册关键字"logout",代表有用户退出,并更新用户列表
                        case "logout":
                            AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
                            RmvItemfromListView(splitString[1]);
                            break;

                        //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
                        case "talk":
                            for (int i = 0; i < chatFormList.Count; i++)
                            {
                                if (chatFormList[i].Text == splitString[2])
                                {
                                    chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
                                }
                            }
                            break;
                    }
                }
                catch
                {
                    break;
                }
            }
            AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
        }

如下图所示:为第一个用户登陆系统时,从状态监控屏幕上看到的客户端与服务器程序的会话过程。
《即时通信聊天工具的原理与设计》
(3) 用户注销

当用用户需要下线退出时,单击客户端界面上的“注销”按钮。

“注销”按钮的过程代码:
//当点击退出按钮的事件处理函数
        private void buttonLogout_Click(object sender, EventArgs e)
        {
            //匿名发送
            sendUdpClient = new UdpClient(0);

            //启动发送线程
            Thread threadSend = new Thread(SendMessage);
            threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
            AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
            receiveUdpClient.Close();
            listViewOnline.Items.Clear();
            buttonLogin.Enabled = true;
            buttonLogout.Enabled = false;
            this.Text = "Client";       //恢复到原来的名字
        }
     注销操作很简单,凡要注销的用户只需向服务器发出“logou”消息,告知服务器就可以了,最好还要关闭客户端自身的UDP套接字。

 服务器在收到“logout”消息后,执行下面代码:  
for (int i = 0; i < userList.Count; i++)
       {
                 if (userList[i].GetName() == splitString[1])
                  {
                                    AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
                                    userList.RemoveAt(i);
                                }
                            }

                            //向所用用户发送更新消息
                            for (int i = 0; i < userList.Count; i++)
                            {
                                SendtoClient(userList[i], message);
                            }
                            AddItemToListBox(string.Format("广播:[{0}]", message));
    服务程序在自己维护的User对象列表中删除这个用户,并且将这个消息广播给所有的用户。

《即时通信聊天工具的原理与设计》

图3 注销时双方的会话

(3) 更新用户列表

 系统内的在线用户收到服务器发来的消息后,实时地更新自己的用户列表。当服务器发来“login”消息时,说明有新成员加入,客户端执行下面的代码:
  AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
  string userItemInfo = splitString[1] + "," + splitString[2];
  AddItemToListView(userItemInfo);
         break;

若收到的是“logout”,则执行下面的代码:

AddItemToListBox(string.Format("用户{ 0}({ 1})退出", splitString[1], splitString[2]));
RmvItemfromListView(splitString[1]);
         break;

为了是程序简单,客户端并没有使用特定的数据结构存储用户列表,而是直接将列表用ListView空间显示在界面上,并用委托机制定义了两个回调函数AddItemToListView()和RmvItemfromListView(),向空间中添加/删除用户信息。

4.3 及时聊天

带有聊天谈话内容的消息以“talk”为首部,采用点对点(P2P)方式发给对方。“talk”消息的发送,接收和显示都由专门的聊天子窗口负责,当客户端主程序收到“talk”的消息时,执行下面的代码:
for (int i = 0; i < chatFormList.Count; i++)
{ if (chatFormList[i].Text == splitString[2]) { chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]); }
}
        break;

系统中的每个用户都对应一个聊天子窗体对象,上段代码的作用就是将一个“talk”消息定位到它的接受者的子窗体对象,再由该对象调用自身的ShwoTalkInfo()方法显示聊天内容。

要打开对应某个用户的子窗口,只需双击在线用户列表中的该用户项即可,代码如下:
//当点击两次发起回话的事件处理函数
        private void listViewOnline_DoubleClick(object sender, EventArgs e)
        {            
            string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
            if (peerName == textBoxUserName.Text)
            {
                return;
            }
            string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
            string[] splitString = ipendp.Split(':');   //除去':'
            IPAddress peerIp = IPAddress.Parse(splitString[0]);
            IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
            ChatForm dlgChatForm = new ChatForm();
            dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
            dlgChatForm.Text = peerName;
            chatFormList.Add(dlgChatForm);
            dlgChatForm.Show();            
        }

其中,chatFormList是客户端程序定义的数据结构,用于保存每一个在线用户的子窗口列表,它与服务器端的userList结构是相对应的。每当用户双击了列表中的某个用户项时,程序就用该项的信息创建一个新的子窗体对象并添加到chatFormList表中。子窗体的初始化使用其自身的SetUserInfo()方法。

哈哈哈哈,现在整个通信聊天软件就完成了,我们看看下面运行的效果。

5.运行效果
同时运行一个服务器(Server)程序和三个客户端(Client)程序,启动服务线程,在三个客户端分别以用户名“泓全”,“爱田”,“爱盼”登陆服务器。如下图所示:
《即时通信聊天工具的原理与设计》
图4 登陆服务器

  此时,每个在线用户两两之间就都可以即时聊天了。不过,在聊天之前,聊天双方要先打开对方的子窗口,操作方法很简单,只要在客户端界面上双击“在线”列表中相应的用户项即可。如下图所示:

《即时通信聊天工具的原理与设计》
图5 在线交谈

6.源代码
6.1 服务器端

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

//添加的命名空间引用
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;

namespace Server
{
    public partial class MainForm : Form
    {
        private List<User> userList = new List<User>(); //保存登录的所有用户
        int  port;                          //服务端口
        int tcport;                         //监听端口
        private UdpClient sendUdpClient;     //匿名发送套接口
        private UdpClient receiveUdpClient;  //实名接收套接口
        private IPEndPoint serverIPEndPoint; //服务器地址
        private TcpListener myTcpListener;   //服务器监听套接口
        private IPAddress serverIp;          //服务器IP
        private NetworkStream networkStream;  //网络流
        private BinaryWriter bw;             //避免出现网络边界问题的写入流
        string userListString;               //用户列表串
        public MainForm()
        {
            InitializeComponent();

            //服务器IP
            IPAddress[] ServerIP = Dns.GetHostAddresses("");
            IPAddress address = IPAddress.Any;
            for (int i = 0; i < ServerIP.Length; i++ )
            {
                if (ServerIP[i].AddressFamily == AddressFamily.InterNetwork)
                {
                    address = ServerIP[i];
                    break;
                }
            }
            textBoxServerIp.Text = address.ToString();

            //随机选择服务端口 Port( Port > 1024 )
            port = new Random().Next(1024, 65535);
            textBoxServerPort.Text = port.ToString();
            buttonStop.Enabled = false;
        }

        //点击开始事件处理函数
        private void buttonStart_Click(object sender, EventArgs e)
        {
            //创建接收套接字
            serverIp = IPAddress.Parse(textBoxServerIp.Text);
            serverIPEndPoint = new IPEndPoint(serverIp, int.Parse(textBoxServerPort.Text));
            receiveUdpClient = new UdpClient(serverIPEndPoint);

            //启动接收线程
            Thread threadReceive = new Thread(ReceiveMessage);
            threadReceive.Start();
            buttonStart.Enabled = false;
            buttonStop.Enabled = true;

            //随机指定监听端口 N( P+1 ≤ N < 65536 )
            Random random = new Random();
            tcport = random.Next(port + 1, 65536);

            //创建监听套接字
            myTcpListener = new TcpListener(serverIp, tcport);
            myTcpListener.Start();

            //启动监听线程
            Thread threadListen = new Thread(ListenClientConnect);
            threadListen.Start();
            AddItemToListBox(string.Format("服务线程({0})启动,监听端口{1}",serverIPEndPoint,tcport));
        }


        //接受客户端连接
        private void ListenClientConnect()
        {
            TcpClient newClient = null;
            while (true)
            {
                try
                {
                    //获得用于传递数据的TCP套接口
                    newClient = myTcpListener.AcceptTcpClient();
                    AddItemToListBox(string.Format("接受客户端{0}的 TCP 请求", newClient.Client.RemoteEndPoint));
                }
                catch
                {
                    AddItemToListBox(string.Format("监听线程({0}:{1})终止", serverIp, tcport));
                    break;
                }

                //启动发送用户列表线程
                Thread threadSend = new Thread(SendData);
                threadSend.Start(newClient);
            }
        }

        //向客户端发送在线用户列表信息
        private void SendData(object userClient)
        {
            TcpClient newUserClient = (TcpClient)userClient;
            userListString = null;
            for (int i = 0; i < userList.Count; i++)
            {
                userListString += userList[i].GetName() + "," + userList[i].GetIPEndPoint().ToString() + ";";
            }
            userListString += "end";
            networkStream = newUserClient.GetStream();
            bw = new BinaryWriter(networkStream);
            bw.Write(userListString);
            bw.Flush();      //不保留现在写入的数据
            AddItemToListBox(string.Format("向{0}传送:[{1}]", newUserClient.Client.RemoteEndPoint, userListString));
            bw.Close();
            newUserClient.Close();
        }

        //接收数据
        private void ReceiveMessage()
        {
            IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
            while (true)
            {
                try
                {
                    //关闭receiveUdpClient时此句会产生异常
                    byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
                    string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);

                    //显示消息内容
                    AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));

                    //处理消息数据
                    string[] splitString = message.Split(',');

                    //解析用户端地址
                    string[] splitSubString = splitString[2].Split(':');   //除去':'
                    IPEndPoint clientIPEndPoint = new IPEndPoint(IPAddress.Parse(splitSubString[0]), int.Parse(splitSubString[1]));
                    switch (splitString[0])
                    {
                        //收到注册关键字"login"
                        case "login":
                            User user = new User(splitString[1], clientIPEndPoint);
                            userList.Add(user);
                            AddItemToListBox(string.Format("用户{0}({1})加入", user.GetName(), user.GetIPEndPoint()));
                            string sendString = "Accept," + tcport.ToString();
                            SendtoClient(user, sendString);   //向该用户发送同意关键字
                            AddItemToListBox(string.Format("向{0}({1})发出:[{2}]", user.GetName(), user.GetIPEndPoint(), sendString));
                            for (int i = 0; i < userList.Count; i++)
                            {
                                if (userList[i].GetName() != user.GetName())
                                {
                                    //向除刚加入的所有用户发送更新消息
                                    SendtoClient(userList[i], message);
                                }
                            }
                            AddItemToListBox(string.Format("广播:[{0}]", message));
                            break;

                        //收到关键字"logout"
                        case "logout":
                            for (int i = 0; i < userList.Count; i++)
                            {
                                if (userList[i].GetName() == splitString[1])
                                {
                                    AddItemToListBox(string.Format("用户{0}({1})退出", userList[i].GetName(), userList[i].GetIPEndPoint()));
                                    userList.RemoveAt(i);
                                }
                            }

                            //向所用用户发送更新消息
                            for (int i = 0; i < userList.Count; i++)
                            {
                                SendtoClient(userList[i], message);
                            }
                            AddItemToListBox(string.Format("广播:[{0}]", message));
                            break;
                    }
                }
                catch
                {
                    break;
                }
            }
            AddItemToListBox(string.Format("服务线程({0})终止", serverIPEndPoint));
        }

        private void SendtoClient(User user, string message)
        {
            //匿名发送
            sendUdpClient = new UdpClient(0);
            byte[] sendbytes = Encoding.Unicode.GetBytes(message);
            IPEndPoint remoteIPEndPoint = user.GetIPEndPoint();
            sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);
            sendUdpClient.Close();
        }

        //当点击关闭按钮的事件处理程序
        private void buttonStop_Click(object sender, EventArgs e)
        {
            myTcpListener.Stop();
            receiveUdpClient.Close();
            buttonStart.Enabled = true;
            buttonStop.Enabled = false;
        }

        //用委托机制解决显示问题
        private delegate void AddItemToListBoxDelegate(string str);
        private void AddItemToListBox(string str)
        {
            if (listBoxStatus.InvokeRequired)
            {
                AddItemToListBoxDelegate d = AddItemToListBox;
                listBoxStatus.Invoke(d, str);
            }
            else
            {
                listBoxStatus.Items.Add(str);
                listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
                listBoxStatus.ClearSelected();
            }
        }
    }
}

6.2 客户端

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

//添加的命名空间引用
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;

namespace Client
{
    public partial class MainForm : Form
    {
        int  port;                           //端口号 
        private UdpClient sendUdpClient;       //匿名发送套接口
        private UdpClient receiveUdpClient;    //实名接收套接口
        private IPEndPoint clientIPEndPoint;   //客户端地址
        private TcpClient myTcpClient;        //TCP套接字
        private NetworkStream networkStream;  //网络流
        private BinaryReader br;             //避免网络边界问题的读数据流
        string userListString;               //用户名字串
        private List<ChatForm> chatFormList = new List<ChatForm>();   //用户窗体列表
        public MainForm()
        {
            InitializeComponent();

            //本地IP和端口号的初始化
            IPAddress[] LocalIP = Dns.GetHostAddresses("");
            IPAddress address = IPAddress.Any;
            for (int i = 0; i < LocalIP.Length; i++)
            {
                if (LocalIP[i].AddressFamily == AddressFamily.InterNetwork)
                {
                    address = LocalIP[i];
                    break;
                }
            }
            textBoxServerIp.Text = address.ToString();
            textBoxLocalIp.Text = address.ToString();

            //获得随机端口号
            port = new Random().Next(1024, 65535);
            textBoxLocalPort.Text = port.ToString();

            //随机生成用户名
            Random r = new Random((int)DateTime.Now.Ticks);   //类似于与C++中的种子
            textBoxUserName.Text = "user" + r.Next(100, 999);
            buttonLogout.Enabled = false;
        }

        private void buttonLogin_Click(object sender, EventArgs e)
        {
            //创建接收套接字
            IPAddress clientIp = IPAddress.Parse(textBoxLocalIp.Text);
            clientIPEndPoint = new IPEndPoint(clientIp, int.Parse(textBoxLocalPort.Text));
            receiveUdpClient = new UdpClient(clientIPEndPoint);

            //启动接收线程
            Thread threadReceive = new Thread(ReceiveMessage);
            threadReceive.Start();
            AddItemToListBox(string.Format("客户线程({0})启动", clientIPEndPoint));

            //匿名发送
            sendUdpClient = new UdpClient(0);

            //启动发送线程
            Thread threadSend = new Thread(SendMessage);
            threadSend.Start(string.Format("login,{0},{1}",textBoxUserName.Text,clientIPEndPoint));
            AddItemToListBox(string.Format("发出:[login,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
            buttonLogin.Enabled = false;
            buttonLogout.Enabled = true;
            this.Text = textBoxUserName.Text;  //使当前窗体名字变为当前用户名
        }

        //发送数据
        private void SendMessage(object obj)
        {
            string message = (string)obj;
            byte[] sendbytes = Encoding.Unicode.GetBytes(message);

            //服务器端的IP和端口号
            IPAddress remoteIp = IPAddress.Parse(textBoxServerIp.Text);
            IPEndPoint remoteIPEndPoint = new IPEndPoint(remoteIp, int.Parse(textBoxServerPort.Text));
            sendUdpClient.Send(sendbytes, sendbytes.Length, remoteIPEndPoint);  //匿名发送
            sendUdpClient.Close();
        }

        //接收数据
        private void ReceiveMessage()
        {
            IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, 0);
            while (true)
            {
                try
                {
                    //关闭receiveUdpClient时此句会产生异常
                    byte[] receiveBytes = receiveUdpClient.Receive(ref remoteIPEndPoint);
                    string message = Encoding.Unicode.GetString(receiveBytes, 0, receiveBytes.Length);

                    //显示消息内容
                    AddItemToListBox(string.Format("{0}:[{1}]", remoteIPEndPoint, message));

                    //处理消息数据
                    string[] splitString = message.Split(',');   //除去','
                    switch (splitString[0])
                    {
                        //若接收连接
                        case "Accept":
                            try
                            {
                                AddItemToListBox(string.Format("连接{0}:{1}...", remoteIPEndPoint.Address, splitString[1]));
                                myTcpClient = new TcpClient();
                                myTcpClient.Connect(remoteIPEndPoint.Address, int.Parse(splitString[1]));
                                if (myTcpClient != null)
                                {
                                    AddItemToListBox("连接成功!");
                                    networkStream = myTcpClient.GetStream();
                                    br = new BinaryReader(networkStream);
                                }
                            }
                            catch
                            {
                                AddItemToListBox("连接失败!");
                            }
                            Thread threadGetList = new Thread(GetUserList);
                            threadGetList.Start(); //请求获得用户列表信息线程启动
                            break;

                            //若收到注册关键字"login",代表有新的用户加入,并更新用户列表
                        case "login":
                            AddItemToListBox(string.Format("新用户{0}({1})加入", splitString[1], splitString[2]));
                            string userItemInfo = splitString[1] + "," + splitString[2];
                            AddItemToListView(userItemInfo);
                            break;

                        //若收到注册关键字"logout",代表有用户退出,并更新用户列表
                        case "logout":
                            AddItemToListBox(string.Format("用户{0}({1})退出", splitString[1], splitString[2]));
                            RmvItemfromListView(splitString[1]);
                            break;

                        //若收到回话关键字"talk",则表明有用户发起回话,并开始准备回话
                        case "talk":
                            for (int i = 0; i < chatFormList.Count; i++)
                            {
                                if (chatFormList[i].Text == splitString[2])
                                {
                                    chatFormList[i].ShowTalkInfo(splitString[2], splitString[1], splitString[3]);
                                }
                            }
                            break;
                    }
                }
                catch
                {
                    break;
                }
            }
            AddItemToListBox(string.Format("客户线程({0})终止", clientIPEndPoint));
        }

        //获得用户列表
        private void GetUserList()
        {
            while (true)
            {
                userListString = null;
                try
                {
                    userListString = br.ReadString();
                    if (userListString.EndsWith("end"))
                    {
                        AddItemToListBox(string.Format("收到:[{0}]", userListString));
                        string[] splitString = userListString.Split(';');
                        for (int i = 0; i < splitString.Length - 1; i++)
                        {
                            AddItemToListView(splitString[i]);
                        }
                        br.Close();
                        myTcpClient.Close();
                        break;
                    }
                }
                catch
                {
                    break;
                }
            }
        }

        //当点击退出按钮的事件处理函数
        private void buttonLogout_Click(object sender, EventArgs e)
        {
            //匿名发送
            sendUdpClient = new UdpClient(0);

            //启动发送线程
            Thread threadSend = new Thread(SendMessage);
            threadSend.Start(string.Format("logout,{0},{1}", textBoxUserName.Text, clientIPEndPoint));
            AddItemToListBox(string.Format("发出:[logout,{0},{1}]", textBoxUserName.Text, clientIPEndPoint));
            receiveUdpClient.Close();
            listViewOnline.Items.Clear();
            buttonLogin.Enabled = true;
            buttonLogout.Enabled = false;
            this.Text = "Client";       //恢复到原来的名字
        }

        //当点击两次发起回话的事件处理函数
        private void listViewOnline_DoubleClick(object sender, EventArgs e)
        {            
            string peerName = listViewOnline.SelectedItems[0].SubItems[1].Text;
            if (peerName == textBoxUserName.Text)
            {
                return;
            }
            string ipendp = listViewOnline.SelectedItems[0].SubItems[2].Text;
            string[] splitString = ipendp.Split(':');   //除去':'
            IPAddress peerIp = IPAddress.Parse(splitString[0]);
            IPEndPoint peerIPEndPoint = new IPEndPoint(peerIp, int.Parse(splitString[1]));
            ChatForm dlgChatForm = new ChatForm();
            dlgChatForm.SetUserInfo(textBoxUserName.Text, peerName, peerIPEndPoint);
            dlgChatForm.Text = peerName;
            chatFormList.Add(dlgChatForm);
            dlgChatForm.Show();            
        }

        //利用委托机制显示信息
        private delegate void AddItemToListBoxDelegate(string str);
        private void AddItemToListBox(string str)
        {
            if (listBoxStatus.InvokeRequired)
            {
                AddItemToListBoxDelegate d = AddItemToListBox;
                listBoxStatus.Invoke(d, str);
            }
            else
            {
                listBoxStatus.Items.Add(str);
                listBoxStatus.TopIndex = listBoxStatus.Items.Count - 1;
                listBoxStatus.ClearSelected();
            }
        }

        private delegate void AddItemToListViewDelegate(string str);
        private void AddItemToListView(string str)
        {
            if (listViewOnline.InvokeRequired)
            {
                AddItemToListViewDelegate d = AddItemToListView;
                listViewOnline.Invoke(d, str);
            }
            else
            {
                string[] splitString = str.Split(',');
                ListViewItem item = new ListViewItem();
                item.SubItems.Add(splitString[0]);
                item.SubItems.Add(splitString[1]);
                listViewOnline.Items.Add(item);
            }
        }

        private delegate void RmvItemfromListViewDelegate(string str);
        private void RmvItemfromListView(string str)
        {
            if (listViewOnline.InvokeRequired)
            {
                RmvItemfromListViewDelegate d = RmvItemfromListView;
                listViewOnline.Invoke(d, str);
            }
            else
            {
                for (int i = 0; i < listViewOnline.Items.Count; i++)
                {
                    if (listViewOnline.Items[i].SubItems[1].Text == str)
                    {
                        listViewOnline.Items[i].Remove();
                    }
                }
            }
        }
    }
}

6.3 聊天子窗口的代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

//添加的命名空间引用
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Client
{
    public partial class ChatForm : Form
    {
        private string selfUserName;          //自己的用户名
        private string peerUserName;          //对方的用户名
        private IPEndPoint peerUserIPEndPoint; //对方的地址
        private UdpClient sendUdpClient;      //匿名发送套接口
        public ChatForm()
        {
            InitializeComponent();
        }

        //类似于构造函数
        public void SetUserInfo(string selfName,string peerName,IPEndPoint peerIPEndPoint)
        {            
            selfUserName = selfName;
            peerUserName = peerName;
            peerUserIPEndPoint = peerIPEndPoint;
        }

        //点击发送按钮的事件处理程序
        private void buttonSend_Click(object sender, EventArgs e)
        {
            //匿名发送
            sendUdpClient = new UdpClient(0);

            //启动发送线程
            Thread threadSend = new Thread(SendMessage);
            threadSend.Start(string.Format("talk,{0},{1},{2}", DateTime.Now.ToLongTimeString(), selfUserName, textBoxSend.Text));            
            richTextBoxTalkInfo.AppendText(selfUserName + "" + DateTime.Now.ToLongTimeString() + Environment.NewLine + textBoxSend.Text);
            richTextBoxTalkInfo.AppendText(Environment.NewLine);
            richTextBoxTalkInfo.ScrollToCaret();            
            textBoxSend.Text = "";
            textBoxSend.Focus();
        }

        //数据发送函数
        private void SendMessage(object obj)
        {
            string message = (string)obj;
            byte[] sendbytes = Encoding.Unicode.GetBytes(message);
            sendUdpClient.Send(sendbytes, sendbytes.Length, peerUserIPEndPoint);
            sendUdpClient.Close();
        }

        //显示通话内容
        public void ShowTalkInfo(string peerName, string time, string content)
        {
            richTextBoxTalkInfo.AppendText(peerName + "" + time + Environment.NewLine + content);
            richTextBoxTalkInfo.AppendText(Environment.NewLine);
            richTextBoxTalkInfo.ScrollToCaret();
        }

        //当点击关闭时的事件处理程序
        private void buttonClose_Click(object sender, EventArgs e)
        {
            this.Close();
        }
    }
}
    原文作者:huasa2239635117
    原文地址: https://blog.csdn.net/huasa2239635117/article/details/78752500
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞