• C#基于Socket的简单聊天室实践


    C#基于Socket的简单聊天室实践

    序:实现一个基于Socket的简易的聊天室,实现的思路如下:

    程序的结构:多个客户端+一个服务端,客户端都是向服务端发送消息,然后服务端转发给所有的客户端,这样形成一个简单的聊天室功能。

    实现的细节:服务端启动一个监听套接字。每一个客户端连接到服务端,都是开启了一个线程,线程函数是封装了通信套接字,来实现与客户端的通信。多个客户端连接时产生的通信套接字用一个静态的Dictionary保存。具体的实现可以参考代码及其注释。

    术语理解:

    套接字Socket:源于Unix,为了解决传输层网络编程的问题,Unix提供了类似于文件操作的方式来完成网络编程。要实现不同的主机,不同的程序之间进行通信,必须有相应的协议,这个协议便是TCP/IP协议。Socket是负责传输层的,基于TCP的。不同的主机之间可以通过IP地址识别,但是不同的主机上有众多的程序或者说是进程。一台主机上的进程要跟另一台主机上的进程通信,必须有双方能够唯一识别的标志。就像人的身份证号,手机号等。这里出现了EndPoint(端点)的概念。

    EndPoint(端点):由IP地址和端口号构成,端口对应进程。这两个组合起来可以唯一的标识网络中某台主机上的某一个进程。这样就有一个唯一的身份标识,后面可以进行通信了。

     每一个Socket需要绑定到端点进行通信。

    Socket的常见的通信数据类型有两种:数据报(SOCK_DGRAM)和数据流(SOCK_STREAM),使用的网络协议TCP或UDP等等。

    关于TCP

    TCP是一种面向连接的,可靠的,基于字节流的传输层通信协议。

    TCP的工作过程包括三个方面:

    (1)建立连接:这个过程称为三次握手。

           第一次:客户端发送SYN包(SEQ=x)到服务器,并进入SYN_SEND状态,等待服务器确认。

           第二次:服务器收到SYN包,必须确认客户端的SYN(ACK=x+1),同时自己发送一个SYN包(SEQ=y),即SYN+ACK包,此时服务器进入SYN_RECV状态

           第三次:客户端收到服务器发来的SYN+ACK包,向服务器发送确认包ACK(ACK=y+1),此包发送完毕,客户端和服务端进入Established状态。至此三次握手完成。

    (2)传输数据:一旦通信双方建立了TCP连接,就可以相互发送数据。

    (3)终止连接:关闭连接,需要四次握手,这个是由于TCP的半关闭造成的。

    这个网上有很多资料,大家可以查阅下。

    关于.NET里面的Socket

    在.NET里面的System.Net.Sockets命名空间下提供了对Socket的操作。并且专门封装了TcpClient和TcpListener两个类来简化操作。我这里是直接用Socket实现的。这里分为这样几个步骤:

    在服务端:

    (1)声明一个套接字(称为监听套接字)Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

    (2)声明一个端点(EndPoint)上面提到过Socket需要跟它绑定才能通信。IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);

    (3)设置监听队列serverSocket.Listen(100);

    (4)通过Accept()方法来获取一个通信套接字(当有客户端连接时),这个方法会阻塞线程,避免界面卡死的现象,启动一个线程,把这个Accept()放在线程函数里面。

    在客户端:

    (1)声明一个套接字,通过connect()向服务器发起连接。

    (2)通过Receive方法获取服务器发来的消息(这里同样启用一个线程,通过while循环来实时监听服务器端发送的消息)

    注意:数据是以字节流(Byte[])的形式传递的,我会使用Encoding.UTF8.GetString()方法来获取为字符串。都是通过Send()来向彼此发送消息。

    聊天室开始

    我这里使用的是WPF来创建的Server和Client端。

    首先看下服务端,我创建一个ChatServer的WPF项目,包含一个ChatServer.xaml:

    复制代码
    <Window x:Class="ChatWPFServer.ChatServer"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="ChatServer" Height="350" Width="525" Closed="Window_Closed">
        <Grid>
            <Button Content="启动" Name="btnStart" HorizontalAlignment="Left" Margin="28,29,0,0" VerticalAlignment="Top" Width="105" Height="39" Click="btnStart_Click" />
            <Button Content="停止" Name="btnStop" HorizontalAlignment="Left" Margin="193,29,0,0" VerticalAlignment="Top" Width="108" Height="39" Click="btnStop_Click" />
            <TextBox Name="txtMsg" HorizontalAlignment="Left" Height="200" AcceptsReturn="True"  Margin="28,98,0,0" IsReadOnly="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Text="" VerticalAlignment="Top" Width="460"/>
    
        </Grid>
    </Window>
    复制代码

    后台代码:

    复制代码
    namespace ChatWPFServer
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class ChatServer : Window
        {
    
            public ChatServer()
            {
                InitializeComponent();
            }
            //保存多个客户端的通信套接字
            public static Dictionary<String, Socket> clientList = null;
            //声明一个监听套接字
            Socket serverSocket = null;
            //设置一个监听标记
            Boolean isListen = true;
            private void btnStart_Click(object sender, RoutedEventArgs e)
            {
                if (serverSocket == null)
                {
                    isListen = true;
                    clientList = new Dictionary<string, Socket>();
                    serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//实例监听套接字
                    IPEndPoint endPoint = new IPEndPoint(IPAddress.Loopback, 8080);//端点
                    serverSocket.Bind(endPoint);//绑定
                    serverSocket.Listen(100);//设置最大连接数
                    Thread th = new Thread(StartListen);
                    th.IsBackground = true;
                    th.Start();
                    txtMsg.Dispatcher.BeginInvoke(new Action(() =>
                    {
                        txtMsg.Text += "服务启动...
    ";
                    }));
                }
            }
    
            //线程函数,封装一个建立连接的通信套接字
            private void StartListen()
            {
                Socket clientSocket = default(Socket);
                try
                {
                    while (isListen)
                    {
                        clientSocket = serverSocket.Accept();//这个方法返回一个通信套接字
                        Byte[] bytesFrom = new Byte[4096];
                        String dataFromClient = null;
                        Int32 len = clientSocket.Receive(bytesFrom);//获取客户端发来的信息
                        if (len > 0)
                        {
                            dataFromClient = Encoding.UTF8.GetString(bytesFrom, 0, len);
                            Int32 sublen = dataFromClient.LastIndexOf("$");
                            if (sublen > -1)
                            {
                                dataFromClient = dataFromClient.Substring(0, sublen);
                                if (!clientList.ContainsKey(dataFromClient))
                                {
                                    clientList.Add(dataFromClient, clientSocket);
    
                                    BroadCast.PushMessage(dataFromClient + " Joined ", dataFromClient, false, clientList);
    
                                    HandleClient client = new HandleClient();
    
                                    client.StartClient(clientSocket, dataFromClient, clientList);
    
                                }
                                else
                                {
                                    clientSocket.Send(Encoding.UTF8.GetBytes("#" + dataFromClient + "#"));
                                }
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
    
                    if (clientSocket != null && !clientSocket.Connected)
                    {
                        clientSocket.Close();
                        clientSocket = null;
                    }
                    isListen = false;
                    File.AppendAllText("E:\Exception.txt", ex.ToString());
                }
    
            }
    
            private void btnStop_Click(object sender, RoutedEventArgs e)
            {
                if (serverSocket != null)
                {
    
                    foreach (var socket in clientList.Values)
                    {
                        socket.Close();
                    }
                    clientList.Clear();
    
                    serverSocket.Close();
                    serverSocket = null;
                    isListen = false;
                    txtMsg.Text += "服务停止
    ";
                }
            }
    
            private void Window_Closed(object sender, EventArgs e)
            {
                isListen = false;
                BroadCast.PushMessage("Server has closed", "", false, clientList);
                clientList.Clear();
                serverSocket.Close();
                serverSocket = null;
            }
    
        }
    
        //这里专门负责接收客户端发来的消息,并且转发给所有的客户端
        public class HandleClient
        {
            Socket clientSocket;
            String clNo;
            Dictionary<String, Socket> clientList = new Dictionary<string, Socket>();
            public void StartClient(Socket inClientSocket, String clientNo, Dictionary<String, Socket> cList)
            {
                clientSocket = inClientSocket;
                clNo = clientNo;
                clientList = cList;
                Thread th = new Thread(Chat);
                th.IsBackground = true;
                th.Start();
            }
    
            private void Chat()
            {
                Byte[] bytesFromClient = new Byte[4096];
                String dataFromClient;
                String msgTemp = null;
                Byte[] bytesSend = new Byte[4096];
                Boolean isListen = true;
                try
                {
    
                    while (isListen)
                    {
                        Int32 len = clientSocket.Receive(bytesFromClient);
    
                        dataFromClient = Encoding.UTF8.GetString(bytesFromClient, 0, len);
                        if (!String.IsNullOrWhiteSpace(dataFromClient))
                        {
                            dataFromClient = dataFromClient.Substring(0, dataFromClient.LastIndexOf("$"));
                            if (!String.IsNullOrWhiteSpace(dataFromClient))
                            {
                                BroadCast.PushMessage(dataFromClient, clNo, true, clientList);
                                msgTemp = clNo + ": " + dataFromClient + "		" + DateTime.Now.ToString();
                                File.AppendAllText("E:\MessageRecords.txt", msgTemp + "
    ", Encoding.UTF8);
                            }
                            else
                            {
                                clientList.Remove(clNo);
                                clientSocket.Close();
                                clientSocket = null;
                                isListen = false;
    
                            }
                        }
                    }
                }
    
                catch (SocketException ex)
                {
                    isListen = false;
                    clientList.Remove(clNo);
                    clientSocket.Close();
                    clientSocket = null;
                    File.AppendAllText("E:\Exception.txt", ex.ToString());
                }
    
    
            }
    
    
        }
    
        //向所有的客户端发送消息
        public class BroadCast
        {
            public static void PushMessage(String msg, String uName, Boolean flag, Dictionary<String, Socket> clientList)
            {
                foreach (var item in clientList)
                {
                    Socket brdcastSocket = (Socket)item.Value;
                    String msgTemp = null;
                    Byte[] castBytes = new Byte[4096];
                    if (flag == true)
                    {
                        msgTemp = uName + ": " + msg + "		" + DateTime.Now.ToString();
                        castBytes = Encoding.UTF8.GetBytes(msgTemp);
                    }
                    else
                    {
                        msgTemp = msg + "		" + DateTime.Now.ToString();
                        castBytes = Encoding.UTF8.GetBytes(msgTemp);
                    }
                    try
                    {
                        brdcastSocket.Send(castBytes);
                    }
                    catch (Exception ex)
                    {
                        brdcastSocket.Close();
                        brdcastSocket = null;
                        File.AppendAllText("E:\Exception.txt", ex.ToString());
                        continue;
                    }
    
                }
            }
        }
    }
    复制代码

    网络编程的环境比较复杂,里面很多地方进行了异常处理和判断。这是通过我实际的调试不断完善的,有的bug甚至是放到了真实的服务器上运行了几个小时以后发现的。所以异常处理最后是记录下来,这样能够便于发现问题。

    客户端xaml:

    复制代码
    <Window x:Class="ChatClient.ChatRoom"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="ChatRoom" Height="550" Width="500"
            Closed="Window_Closed_1" Activated="Window_Activated_1" WindowStartupLocation="CenterScreen">
        <Window.Resources>
            <RoutedUICommand x:Key="SendMessageShortcutKey" Text="Send Message Shortcut Key"></RoutedUICommand>
        </Window.Resources>
        <Window.InputBindings>
            <KeyBinding Gesture="Ctrl+Enter" Command="{StaticResource SendMessageShortcutKey}"></KeyBinding>
        </Window.InputBindings>
        <Window.CommandBindings>
            <CommandBinding Command="{StaticResource SendMessageShortcutKey}" CanExecute="CommandBinding_SendMessage_CanExecute" Executed="CommandBinding_SendMessage_Executed">
            </CommandBinding>
        </Window.CommandBindings>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="50"></RowDefinition>
                <RowDefinition Height="250"></RowDefinition>
                <RowDefinition Height="150"></RowDefinition>
                <RowDefinition Height="80"></RowDefinition>
            </Grid.RowDefinitions>
            <StackPanel Grid.Row="0" Orientation="Horizontal">
                <Label  Name="lbMsg" Content="设置你的用户名:" Margin="10,10"></Label>
                <TextBox Name="txtName" Width="200" Height="30" FontSize="16"></TextBox>
                <Button Name="btnConnect" Content="连接服务器" Width="100" Height="30" Margin="15,8" Click="btnConnect_Click"></Button>
            </StackPanel>
            <TextBox Grid.Row="1" Name="txtReceiveMsg" Margin="10" BorderBrush="DarkGreen" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Foreground="DarkBlue" IsReadOnly="True"></TextBox>
            <TextBox Grid.Row="2" Name="txtSendMsg" Margin="10,0" AcceptsReturn="True" BorderBrush="DarkBlue" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto"></TextBox>
            <Button Grid.Row="3" Name="btnSend" Content="发送(Ctrl+Enter)" Width="100" Height="30" Click="btnSend_Click"></Button>
        </Grid>
    </Window>
    复制代码

    后台代码:

    复制代码
    namespace ChatClient
    {
        /// <summary>
        /// Interaction logic for MainWindow.xaml
        /// </summary>
        public partial class ChatRoom : Window
        {
            public ChatRoom()
            {
                InitializeComponent();
            }
            
            Socket clientSocket = null;
            static Boolean isListen = true;
            private void btnSend_Click(object sender, RoutedEventArgs e)
            {
                SendMessage();
            }
    
            private void SendMessage()
            {
                if (String.IsNullOrWhiteSpace(txtSendMsg.Text.Trim()))
                {
                    MessageBox.Show("发送内容不能为空哦~");
                    return;
                }
                if (clientSocket != null && clientSocket.Connected)
                {
                    Byte[] bytesSend = Encoding.UTF8.GetBytes(txtSendMsg.Text + "$");
                    clientSocket.Send(bytesSend);
                    txtSendMsg.Text = "";
                }
                else
                {
                    MessageBox.Show("未连接服务器或者服务器已停止,请联系管理员~");
                    return;
                }
            }
    
            /// <summary>
            /// 每一个连接的客户端必须设置一个唯一的用户名,在服务端是把用户名和通信套接字
            /// 保存在Dictionary<UserName,ClientSocket>.
            /// </summary>
            /// <param name="sender"></param>
            /// <param name="e"></param>
            private void btnConnect_Click(object sender, RoutedEventArgs e)
            {
                if (String.IsNullOrWhiteSpace(txtName.Text.Trim()))
                {
                    MessageBox.Show("还是设置一个用户名吧,这样别人才能认识你哦~");
                    return;
                }
    
                if (clientSocket == null)
                {
                    try
                    {
                        clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                        clientSocket.BeginConnect(IPAddress.Loopback, 8080, (args) =>
                        {
                            if (args.IsCompleted)
                            {
                                Byte[] bytesSend = new Byte[4096];
                                txtName.Dispatcher.BeginInvoke(new Action(() =>
                                {
                                    bytesSend = Encoding.UTF8.GetBytes(txtName.Text.Trim() + "$");
                                    if (clientSocket != null && clientSocket.Connected)
                                    {
                                        clientSocket.Send(bytesSend);
                                        txtName.IsEnabled = false;
                                        txtSendMsg.Focus();
                                        Thread th = new Thread(DataFromServer);
                                        th.IsBackground = true;
                                        th.Start();
    
                                    }
                                    else
                                    {
                                        MessageBox.Show("服务器已经关闭");
                                    }
                                }));
    
    
                            }
                        }, null);
    
                    }
                    catch (SocketException ex)
                    {
                        MessageBox.Show(ex.ToString());
                    }
    
                }
                else
                {
                    MessageBox.Show("You has already connected with Server");
                }
            }
    
            private void ShowMsg(String msg)
            {
                txtReceiveMsg.Dispatcher.BeginInvoke(new Action(() =>
                {
                    txtReceiveMsg.Text += Environment.NewLine + msg;
                    txtReceiveMsg.ScrollToEnd();
                }));
    
            }
    
            //获取服务端的消息
            private void DataFromServer()
            {
                ShowMsg("Connected to the Chat Server...");
                isListen = true;
                try
                {
    
                    while (isListen)
                    {
                        Byte[] bytesFrom = new Byte[4096];
                        Int32 len = clientSocket.Receive(bytesFrom);
    
    
                        String dataFromClient = Encoding.UTF8.GetString(bytesFrom, 0, len);
                        if (dataFromClient.StartsWith("#") && dataFromClient.EndsWith("#"))
                        {
                            String userName = dataFromClient.Substring(1, dataFromClient.Length - 2);
                            this.Dispatcher.BeginInvoke(new Action(() =>
                            {
                                MessageBox.Show("用户名:[" + userName + "]已经存在,请尝试其它用户名");
                            }));
                            isListen = false;
    
                        }
                        else
                        {
    
                            ShowMsg(dataFromClient);
                        }
    
                    }
                }
                catch (SocketException ex)
                {
                    isListen = false;
                    if (clientSocket != null && clientSocket.Connected)
                    {
                        //我没有在客户端关闭连接而是向服务端发送一个消息,在服务器端关闭,这样主要
                        //为了异常的处理放到服务端。客户端关闭会抛异常,服务端也会抛异常。
                        clientSocket.Send(Encoding.UTF8.GetBytes("$"));
                        MessageBox.Show(ex.ToString());
                    }
    
                }
            }
    
            //这是定义了一个发送的快捷键,WPF的知识
            private void CommandBinding_SendMessage_CanExecute(object sender, CanExecuteRoutedEventArgs e)
            {
                if (String.IsNullOrWhiteSpace(txtSendMsg.Text.Trim()))
                {
                    e.CanExecute = false;
                }
                else
                {
                    e.CanExecute = true;
                }
    
            }
    
            private void CommandBinding_SendMessage_Executed(object sender, ExecutedRoutedEventArgs e)
            {
                SendMessage();
            }
    
            private void Window_Activated_1(object sender, EventArgs e)
            {
    
                txtSendMsg.Focus();
    
            }
    
            private void Window_Closed_1(object sender, EventArgs e)
            {
                if (clientSocket != null && clientSocket.Connected)
                {
                    clientSocket.Send(Encoding.UTF8.GetBytes("$"));
                }
            }
        }
    }
    复制代码

    好了,到这里告一段落了,一个简单的不能再简单的聊天室就完成了。这里还是有很多细节的处理,我不一定处理的很到位,如果你发现了,请留言告诉我。

    截个图,看下运行的效果吧!

    作者:Gabriel Zhang
    出处:http://www.cnblogs.com/mszhangxuefei/
    本文版权归作者和博客园共有,欢迎转载,但必须注明出处,并在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

    要走的远,就要一起走

     
    分类: C#.NET点滴
  • 相关阅读:
    61. Rotate List
    60. Permutation Sequence
    59. Spiral Matrix II
    57. Insert Interval
    18多校8th
    2019山东省赛总结
    二分图——poj2239
    二分图匹配——poj1469
    二分图——poj2446匈牙利算法
    思维构造,建图——cf1159E
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/3222389.html
Copyright © 2020-2023  润新知