初学网络通信编程时应该注意的常见问题

首先让我们写一个C/S多线程网络程序,通过这个程序了解初学网络编程是应该注意的几个问题。

下面开始设计服务器端的业务逻辑代码:

首先,值得强调的是,要在该文件的顶端添加多线程和网络的命名空间语句:



using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
//可以套接字
using System.Net.Sockets;
//可以使用多线程
using System.Threading;



接写来,设计类Threadtcpserver。代码如下:

class Threadtcpserver
    {
        private Socket server;
        public Threadtcpserver()
        {
            //初始化IP地址
            IPAddress local = IPAddress.Parse("127.0.0.1");
            IPEndPoint iep = new IPEndPoint(local, 13000);
            server = new Socket(AddressFamily.InterNetwork,
                SocketType.Stream, ProtocolType.Tcp);
            //将套接字与本地终结点绑定
            server.Bind(iep);
            //在本地13000端口号上进行监听
            server.Listen(20);
            Console.WriteLine("等待客户机进行连接...");
            while (true)
            {
                //得到包含客户端信息的套接字
                Socket client = server.Accept();
                //创建消息线程服务对象
                ClientThread newclient = new ClientThread(client);
                //把ClientThread类的ClientService方法委托给线程
                Thread newthread = new Thread(new ThreadStart(newclient.ClientService));
                //启动消息服务进程
                newthread.Start();

            }

        }
   
    }

上面这段代码的业务逻辑是:

1.创建套接字Server,并将其与本地这结点iep进行绑定。然后,在13000端口上监听是否有新的客户端进行连接。

2.在无限循环中有一个阻塞的方法Accept(),该方法直到有新客户端连接到服务器上时,把客户端的套接字信息传递给client对象。否则,将阻塞直到有客户机进行连接。

3.ClientThread类负责处理客户端与服务器之间的通信。先把客户端的套接字句柄传递给负责消息服务的ClientThread类。然后,把ClientThread类的ClientService方法委托给线程,并启动线程。



然后开始编写ClientThread类的代码:

class ClientThread
    {
        //connection变量表示连接数
        public static int connection = 0;
        public Socket service;
        int i;
        //构造函数
        public ClientThread(Socket clientsocket)
        {
            //service对象接管对消息的控制
            this.service = clientsocket;
        }

        public void ClientService()
        {
            String data = null;
            byte[] bytes = new byte[1024];
            //如果Socket不是空,则连接数加1
            if (service != null)
            {
                connection++;
            }
            Console.WriteLine("新客户建立连接:{0}个连接数", connection);
            while ((i=service.Receive(bytes)) != 0)
            {
                data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
                Console.WriteLine("收到的数据:{0}", data);
                //处理客户端发来的消息,这里转化为大写字母
                data = data.ToUpper();
                byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
                //发送一条答应消息
                service.Send(msg);
                Console.WriteLine("发送的数据:{0}", data);
            }
            //关闭套接字
            service.Close();
            connection--;
            Console.WriteLine("客户关闭连接:{0}个连接数", connection);
        }



这段代码的业务逻辑:

1.在构造函数中得到接收到的客户端套接字client,以后就通过service句柄处理消息的接收和发送。

2.ClientService方法是委托给线程的,此方法进行消息的处理工作。在这里实现的功能是,先从客户端接收一条消息,然后把这条消息转换为大写字母,并立即发送一条应答的消息,也就是所谓的echo技术,通常用来进行消息之间的测试。

3.通过connections变量来记录活动连接数。当有新增连接或有断开连接的情况发生时,都会体现connection是的变化。



主函数代码如下:

        static void Main(string[] args)
        {
            Threadtcpserver instance = new Threadtcpserver();
        }

Main函数十分简单,生成一个Threadtcpsever实例,然后构造函数就会一步一步地展开,开始执行具体的业务逻辑。



编写完服务器端的程序后,只要再配置一个客户端程序即可。下面编写客户端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            Socket client;
            byte[] buf = new byte[1024];
            string input;
            IPAddress local = IPAddress.Parse("127.0.0.1");
            IPEndPoint iep = new IPEndPoint(local, 13000);
            try
            {
                client = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                client.Connect(iep);
            }
            catch (SocketException)
            {
                Console.WriteLine("无法连接到服务器!");
                return;
            }
            finally
            {
              
            }
            while (true)
            {
                //在控制台上输入一条消息
                input = Console.ReadLine();
                //如果输入exit,可以断开服务器的连接
                if (input == "exit")
                {
                    break;
                }
                client.Send(Encoding.ASCII.GetBytes(input));
                //得到实际收到的字节总数
                int rec = client.Receive(buf);
                Console.WriteLine(Encoding.ASCII.GetString(buf, 0, rec));
            }
            Console.WriteLine("断开与服务器的链接");
            client.Close();
        }
    }
}

客户端是比较简单的,只有一个Main()函数。下面来分析一下这段代码的功能:

1.创建套接字,并通过connect方法连接到本地终结点。当连接建立以后,便可以与服务器进行通信了。

2.在客户端上等待用户输入一条信息,该消息会发送到服务器创建的消息服务线程上的ClientService方法进行处理。



运行界面:






















让网络通信代码更强壮



通常,程序都需要进行后期维护和扩展。下面将通过在程序中加入异常处理的机制来加强该程序的健壮性。可以通过使用try和catch结构来包含可能会引发错误的代码。



    try
            {
                client = new Socket(AddressFamily.InterNetwork,
                    SocketType.Stream, ProtocolType.Tcp);
                client.Connect(iep);
            }
            catch (SocketException)
            {
                Console.WriteLine("无法连接到服务器!");
                return;
            }
            finally
            {
                Socket.close();
            }



数据缓冲区处理方法

对于网络编程来说,如何处理好缓冲区这个问题一直是颇具有挑战性的。

1.合理的缓冲区处理

在多线程那段代码里,我们并没有对消息处理的细节做深入的分析,现在来看一下到底是怎么做的。

  while ((i=service.Receive(bytes)) != 0)
            {
                data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
                Console.WriteLine("收到的数据:{0}", data);
                //处理客户端发来的消息,这里转化为大写字母
                data = data.ToUpper();
                byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
                //发送一条答应消息
                service.Send(msg);
                Console.WriteLine("发送的数据:{0}", data);
            }

这里的一个细节是,通过使用变量i得到实际从缓冲区取到的数据总数。在吧字节数组bytes转换成字符串data的过程中,可以清晰的看到,使用了GetString函数的带三个参数版本的重载方法。这样做有什么道理吗?如果把GetString方法的后两个参数去掉,会有什么结果?



不合理的缓冲区处理

在上文的代码中,去掉后两个参数并测试。




我们看到服务器端在接收到数据后多出了很多空行。

真正的原因是Recive方法会一次性把系统缓冲区内的内容取到数据缓冲区,得到数据量取决于缓冲区的大小,即bytes的大小。



如果把缓冲区大小设置的过小会出现什么状况呢?

我们把bytes重新设置:byte[] bytes = new byte[2];




当输入TEST时,系统会分两次进行数据传送。。即调用了两次Recive方法。



所以在处理缓冲区问题是,必须将问题考虑周全。

你可能感兴趣的:(多线程,编程,.net,socket,LINQ)