【黑马程序员】Socket编程实现服务端和客户端的交互


---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------


(注:本文代码是学习笔记,仅实现其功能,还有很多bug)

用Socket实现网络编程首先要创建一个Socket对象,Socket类位于System.Net.Socket命名空间,需要先行导入。创建Socket对象需要以下三个参数,这些参数都是枚举类型:

①AddressFamily成员指定Socket用来解析地址的寻址方案,例如:InternetWork指示当Socket使用一个IP版本4地址连接;

②SocketType定义一个要打开的Socket类型;

③Socket类使用ProtocolType枚举向Windows Sockets API通知所请求的协议。

如下代码为服务端创建Socket的代码:

public partial class FrmService : Form     //服务端窗体
    {
        Socket listenSocket = null;   //服务器端负责监听的套接字

        Thread connThread = null;   //负责监听客户端连接请求的线程

        public FrmService()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;  //关闭对文本框的跨线程操作检查
        }

        private void btnBeginListen_Click(object sender, EventArgs e)
        {
            //创建服务器端负责监听的套接字,参数(使用IP4寻址协议,使用流式连接,使用TCP协议传输数据)
            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //用文本框中的IP地址创建一个IP地址对象IPAddress
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            //创建一个包含IP地址和端口的网络节点对象
            IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
            //把网络节点对象绑定到负责监听的Socket上
            listenSocket.Bind(endpoint);
            //设置监听队列的长度,也就是当前可以同时监听的请求次数
            listenSocket.Listen(10);
            //创建负责监听的线程对象
            connThread = new Thread(WatchConnection);
            //把该线程设置为后台线程
            connThread.IsBackground = true;
            //开启线程
            connThread.Start();
            txtShow.Text = "服务端启动监听成功!";
        }

        /// <summary>
        /// 监听客户端连接事件
        /// </summary>
        private void WatchConnection()
        {
            while (true)  //使用一个死循环持续不断的监听新的客户端的连接请求
            {
                //开始监听客户端的连接请求
                Socket connSocket = listenSocket.Accept();
                ShowMsg("客户端连接成功!");
            }
        }

        private void ShowMsg(string message)
        {
            txtShow.Text = message + "\r\n";
        }
    }
由于Socket监听客户端连接请求的Accept()方法会阻断当前线程的执行,直到接收到客户端的连接信息为止,所以我们要另外创建一个后台线程来实现监听功能而又不影响我们对主窗体的操作。在这个后台线程的委托事件中,需要持续不断地调用Accept()方法实现不断监听。在上面代码中,有一句:listenSocket.Listen(10); ,这句代码的意思是设置当前监听Socket的监听队列,每次只能同时监听10个客户端的连接请求。

服务端负责监听的Socket在使用Accept()方法监听到客户端的连接请求之后,会马上返回一个新的Socket来用于和这个客户端进行通讯。而又由于客户端的数量不确定,因此这里需要一个集合来专门存储Accept()方法返回的与客户端进行通讯的Socket。这样才能让该服务端同时连接上多个客户端。

经过改善后的代码如下:

public partial class FrmService : Form     //服务端窗体
    {
        Socket listenSocket = null;   //服务器端负责监听的套接字

        Thread connThread = null;   //负责监听连接的线程
        Dictionary<string, Thread> recThreads = new Dictionary<string, Thread>();    //专门负责接收消息的线程

        //Socket connSocket = null;  //服务端负责与客户端通讯的Socket

        //专门用于存储//服务端负责与客户端通讯的Socket的集合
        Dictionary<string, Socket> connSockets = new Dictionary<string, Socket>();

        IPEndPoint endpoint = null;   //服务端的IP端口

        public FrmService()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;  //关闭对文本框的跨线程操作检查
        }

        private void btnBeginListen_Click(object sender, EventArgs e)
        {
            //创建服务器端负责监听的套接字,参数(使用IP4寻址协议,使用流式连接,使用TCP协议传输数据)
            listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //用文本框中的IP地址创建一个IP地址对象IPAddress
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            //创建一个包含IP地址和端口的网络节点对象
            endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
            //把网络节点对象绑定到负责监听的Socket上
            listenSocket.Bind(endpoint);
            //设置监听队列的长度,也就是当前可以同时监听的请求次数
            listenSocket.Listen(10);
            //创建负责监听的线程对象
            connThread = new Thread(WatchConnection);
            //把该线程设置为后台线程
            connThread.IsBackground = true;
            //开启线程
            connThread.Start();
            ShowMsg("服务端启动监听成功!");
        }

        /// <summary>
        /// 监听客户端连接事件
        /// </summary>
        private void WatchConnection()
        {
            while (true)  //使用一个死循环持续不断的监听新的客户端的连接请求
            {
                //开始监听客户端的连接请求
                Socket connSocket = listenSocket.Accept();
                //向ListBox中添加一个IP端口字符串,作为访问该客户端的唯一标志
                lbUniqueSign.Items.Add(connSocket.RemoteEndPoint.ToString());
                //将与客户端通讯的Socket添加到集合中
                connSockets.Add(connSocket.RemoteEndPoint.ToString(), connSocket);
                Thread thread = new Thread(ReciveMessage);
                thread.IsBackground = true;
                //以IP端口字符串为Key值,把接收消息的线程添加到recThread集合中。
                recThreads.Add(connSocket.RemoteEndPoint.ToString(), thread);
                thread.Start(connSocket);   //传入参数,这个参数是当前负责与这个客户端进行通讯的Socket
                ShowMsg("客户端连接成功!" + connSocket.RemoteEndPoint.ToString());
                
            }
        }

        private void ShowMsg(string message)
        {
            txtShow.AppendText(message + "\r\n");
        }

        /// <summary>
        /// 发送消息按钮
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSendMsg_Click(object sender, EventArgs e)
        {
            //把发送的消息转换为字节数组
            byte[] arrSendMsg = Encoding.UTF8.GetBytes(txtMessage.Text.Trim());
            //按照选定的IP端口,把消息发送到该客户端上
            connSockets[lbUniqueSign.SelectedItem.ToString()].Send(arrSendMsg);
            //显示消息
            ShowMsg(endpoint.ToString() + "发送消息:");
            ShowMsg("\t"+txtMessage.Text.Trim());
        }        

        /// <summary>
        /// 循环接收客户端发送过来的数据
        /// </summary>
        private void ReciveMessage(object socketParam)       //接收消息的方法需要一个object类型的参数
        {
            Socket socketClient = socketParam as Socket;   //把object类型转换为Socket类型
            while (true)
            {
                //声明一个2M空间的字节数组
                byte[] arrRecMsg = new byte[1024 * 1024 * 2];
                //把接收到的字节存入字节数组中,并获取接收到的字节数
                int length = socketClient.Receive(arrRecMsg);
                //按照接收到的实际字节数获取发送过来的消息
                ShowMsg(socketClient.RemoteEndPoint.ToString() + ":");
                ShowMsg("\t" + Encoding.UTF8.GetString(arrRecMsg, 0, length));
            }
        }
    }

以上代码在改善之后,又添加了一个发送消息方法。发送消息需要调用Socket的Send()方法,它需要一个byte类型的数组参数。我们需要先把要发送的消息转换为byte类型的数组,使用Encoding.UTF8.GetBytes()方法,传入消息参数即可,然后把这个数组传入Send()方法中。上面代码还实现了接收客户端信息的功能,我们把接收消息也放在一个单独的后台线程中。(具体原因在下面)

我们需要从客户端去连接服务端,因此在客户端,我们需要创建一个专门连接服务端的Socket,并传入服务端的IP端口。代码如下:

public partial class FrmClient : Form     //客户端窗体
    {
        private Thread recThread = null;   //负责持续获取服务端发来的消息

        private Socket socketClient = null;  //创建客户端负责连接的Socket对象

        public FrmClient()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }

        private void btnConnectService_Click(object sender, EventArgs e)
        {
            //创建一个IPAddress对象
            IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
            //创建一个包含IP地址和端口的网络节点IPEndPoint对象
            IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
            //创建Socket对象
            socketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            //传入网络节点,连接服务端
            socketClient.Connect(endpoint);
            //创建获取消息的后台线程对象
            recThread = new Thread(ReciveMessage);
            recThread.IsBackground = true;
            //开启线程
            recThread.Start();

        }

        /// <summary>
        /// 循环接收服务端发送过来的数据
        /// </summary>
        private void ReciveMessage()
        {
            while (true)
            {
                //声明一个2M空间的字节数组
                byte[] arrRecMsg = new byte[1024 * 1024 * 2];
                //把接收到的字节存入字节数组中,并获取接收到的字节数
                int length = socketClient.Receive(arrRecMsg);
                //按照接收到的实际字节数获取发送过来的消息
                ShowMsg(socketClient.RemoteEndPoint.ToString() + ":");
                ShowMsg("\t" + Encoding.UTF8.GetString(arrRecMsg, 0, length));
            }
        }

        private void ShowMsg(string message)
        {
            txtShow.AppendText(message + "\r\n");
        }

        /// <summary>
        /// 发送消息按钮
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSendMsg_Click(object sender, EventArgs e)
        {
            byte[] arrSendMsg = Encoding.UTF8.GetBytes(txtMessage.Text.Trim());
            socketClient.Send(arrSendMsg);
            ShowMsg("客户端" + "发送消息:");
            ShowMsg("\t"+txtMessage.Text.Trim());
        }
    }

由于Socket的Recive()方法会阻碍线程的执行,因此也需要新建一个后台线程来接收服务端的信息。这个方法需要一个byte类型的数组来存储接收的字节,然后返回总的字节数。我们可以事先声明一个2M的byte类型的数组。然后我们需要把这些字节转换为字符串,可以使用Encoding.UTF8.GetString()方法,这个方法需要3个参数,一个是存储信息的byte类型的数组,一个是开始转换的字节数,最后一个是需要转换的字节长度。如果不设置需要转换的字节长度,那么程序就会把2M的空间的byte类型数组全部转换,这样会导致资源的浪费和接收消息的不正确。在上面的代码中我们还实现了从客户端向服务端发送信息。由于客户端已经有一个连接服务端的Socket对象,因此直接调用该对象的Send()方法就可以了。

同时启动服务端和客户端两个程序(先启动其中一个项目,然后在解决方案管理器的另外一个项目名上单击右键,在弹出的快捷菜单中选择“调试”里面的“启动新实例”项并单击),运行结果如下图:




---------------------- ASP.Net+Android+IOS开发.Net培训、期待与您交流! ----------------------


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值