我想在我非常简单的服务器应用程序上进行代码审查,以验证从客户端检索到的序列号是否有效。


是否有更好的方法来处理服务的启动/停止?如果您查看我的Start和Service方法,则基本上是查看isServerRunning布尔变量,并在将服务切换为false时将服务退出while循环,但这似乎不是理想的方法,因为线程是基本上在listner.AcceptSocket()处暂停。
是否可以确保N:1的连接,其中1个是服务器?
我要在套接字上设置发送/接收超时。一般设置合理的超时值是什么?

我还想听听有关整体编码风格的评论以及有关改进此代码的其他建议。

public partial class ServerForm : Form
{
    #region Fields
    private bool isServerRunning = false;
    private const int CLIENT_LIMIT = 10;
    private TcpListener listener;

    #endregion


    #region Event Handlers

    private void btnStart_Click(object sender, EventArgs e)
    {
        try
        {
            int port;

            if (String.IsNullOrEmpty(txtPort.Text))
            {
                MessageBox.Show(Constant.ERROR_PORT_NUMBER_EMPTY);
                return;
            }

            if (isServerRunning)
                return;

            if (!int.TryParse(txtPort.Text, out port))
            {
                MessageBox.Show(Constant.ERROR_PORT_ONLY_INT_ALLOWED);
                return;
            }

            if (port < 0 || port > 65535)
            {
                MessageBox.Show(Constant.ERROR_PORT_NUMBER_OUT_OF_RANGE);
                return;
            }

            Start(port);
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }

    private void btnStop_Click(object sender, EventArgs e)
    {
        if (isServerRunning)
        {
            isServerRunning = false;
            ServerLogWriter("Server Stopped");
        }
    }

    private void btnClear_Click(object sender, EventArgs e)
    {
        try
        {
            listBoxLog.Items.Clear();
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }


    #endregion

    #region Constructor
    public ServerForm()
    {
        InitializeComponent();
    }
    #endregion


    #region Private Methods

    private void ServerLogWriter(string content)
    {
        string log = DateTime.Now + " : " + content;
        this.BeginInvoke(new Action(() => 
        {
           listBoxLog.Items.Add(log);
        }
        ));
        LogWriter.WriteLog(log, Constant.NETWORK_LOG_PATH);
    }

    private void Start(int port)
    {
        try
        {
            isServerRunning = true;
            listener = new TcpListener(IPAddress.Parse(LocalIPAddress()), port);
            listener.Start();

            for (int i = 0; i < CLIENT_LIMIT; i++)
            {
                Thread t = new Thread(new ThreadStart(Service));
                t.IsBackground = true;
                t.Start();
            }
            ServerLogWriter(String.Format("Server Start (Port Number: {0})", port));

        }
        catch (SocketException ex)
        {
            ServerLogWriter(String.Format("Server Start Failure (Port Number: {0}) : {1}",port,ex.Message));
            LogWriter.WriteExceptionLog(ex.ToString());
            isServerRunning = false;
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
            isServerRunning = true;
        }
    }
    private void Service()
    {
        try
        {
            while (isServerRunning)
            {
                Socket soc = listener.AcceptSocket();

                ServerLogWriter(String.Format("Client Connected : {0}", soc.RemoteEndPoint));

                try
                {
                    Stream s = new NetworkStream(soc);
                    StreamReader sr = new StreamReader(s);
                    StreamWriter sw = new StreamWriter(s);
                    sw.AutoFlush = true;

                    string validation = String.Empty;
                    string serial = sr.ReadLine();
                    if (ValidateSerial(serial))
                    {
                        validation = String.Format("Serial Number Validated (Received Serial: {1}) : {0}", soc.RemoteEndPoint, serial);
                        sw.WriteLine("VALIDATE");
                    }
                    else
                    {
                        validation = String.Format("Invalid Serial Number (Received Serial: {1}) : {0}", soc.RemoteEndPoint, serial);
                        sw.WriteLine("FAILURE");
                    }
                    ServerLogWriter(validation);
                    s.Close();
                }
                catch (Exception ex)
                {
                    ServerLogWriter(String.Format("Socket Error: {0}, {1}", soc.RemoteEndPoint, ex.Message));
                    LogWriter.WriteLog(ex.ToString(), Constant.EXCEPTION_LOG_PATH);
                }

                ServerLogWriter(String.Format("End Connection : {0}", soc.RemoteEndPoint));

                soc.Close();
            }
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }
    }

    private string LocalIPAddress()
    {
        string localIP = String.Empty;
        try
        {
            IPHostEntry host;
            host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (IPAddress ip in host.AddressList)
            {
                if (ip.AddressFamily.ToString() == "InterNetwork")
                {
                    localIP = ip.ToString();
                }
            }
            return localIP;
        }
        catch (Exception ex)
        {
            LogWriter.WriteExceptionLog(ex.ToString());
        }

        return localIP;
    }

    #endregion
}


评论

对于序列号验证,使用WCF是否更合适?您有没有选择?

请参见下面的工作示例。C#套接字示例程序非常简单,对初学者来说很好。

#1 楼

您可能需要查看有关此内容的其他指南/实现: C#SocketAsyncEventArgs高性能SocketStack溢出|如何编写可扩展的基于Tcp / Ip的服务器

关于您的代码:


您应该将UI和服务器代码分开,以便能够在控制台应用程序或更可能是Windows服务上使用它。

TcpListener是一门不错的课程,可以很好地完成其工作,但是如果您想要更具可扩展性和可定制性的产品,我认为它对您来说太高级了。为此,您应该直接处理Socket类。

您还应该知道,
有太多概念,一般而言,您应该了解TCP和套接字编程。为了编写一个健壮的,可扩展的TCP服务器。您应该了解框架协议,找到处理缓冲区的好方法,对异步代码和调试该代码有经验。

编辑:Stephen Cleary的FAQ页面是一个很好的入门指南。

我建议您看一下我上面提到的链接。他们提供了一些很好的实现,并解释了为什么要以综合的方式来做自己的事情。然后,您可以自己滚动,避免常见的陷阱。

回到您的代码:


为每个连接创建一个新线程是残酷的。您应该使用TcpListener类的异步方法。或者,您至少可以使用ThreadPool,而最简单的方法是使用Task类。

LocalIPAddress()方法将字符串再次解析为要使用的IPAddress对象。不要将其转换为字符串,直接将其作为IPAddress返回。

AddressFamilyEnum。您可以在不将其转换为字符串的情况下检查它是否为InterNetworkip.AddressFamily == AddressFamily.InterNetwork

尝试在using块中使用流,或将它们放置在finally块中,这样即使有异常,也可以确保它们已关闭在您的代码中抛出。
超时时间实际上取决于客户端,网络和您要发送/接收的数据大小。无论您认为闲置时间多长,都意味着“出了点问题”将是足够的超时值。

尽管如此:
我的建议是不要继续进行此工作并在研究了一些问题后再开始其他实现,例如我提到的实现。您需要一个独立于UI的项目,其他需要使用(或必须是)TCP服务器的应用程序可以引用该项目。

编辑:尽管我说过您应该可以,但它并不意味着您应该在GUI应用程序中拥有一个TCP服务器。您需要Windows服务在后台运行,处理大量连接并管理其自身与客户端之间的数据流量。在GUI应用程序中执行此类操作通常是个坏主意。

评论


\ $ \ begingroup \ $
好的。感谢您的好答案。我将GUI代码与服务器代码混合的原因是对我的要求之一是在列表框中显示连接状态的日志。关于如何处理此问题有什么好主意吗?
\ $ \ endgroup \ $
–l46kok
2012年10月24日,0:09

\ $ \ begingroup \ $
@ l46kok:它必须是一个ListBox吗?您可以将日志写入数据库或日志文件,然后可以创建GUI应用程序(例如LogViewer)来查询和显示日志。无论如何,您不应该将所有日志都保留在内存中,因为这最终会导致程序抛出OutOfmemoryException,并且有一个事实,当您关闭应用程序时,所有日志都将消失。对于LogViewer,应根据您的环境获取最后N条日志。 (我认为包含数百万个项目的ListBox效果不佳)
\ $ \ endgroup \ $
–ŞafakGür
2012-10-24 6:00



\ $ \ begingroup \ $
@ŞafakGür听起来很奇怪,是的,它必须是一个ListBox,因为这是对我的要求。当列表框达到一定数量的项目时,它会自动清除,因此内存并不是什么大问题。
\ $ \ endgroup \ $
–l46kok
2012年10月24日在6:16

\ $ \ begingroup \ $
@ l46kok:然后您可以在日志查看器中使用它。如果需要,您应该能够看到昨天或上个月的日志。因此,将日志保存在更持久的位置(例如数据库或文件系统)是可行的方法。您可以使用任何希望在日志查看器中显示日志的控件。这样想:您的TCP服务器将一直运行,但并不是每个人都每秒不断地查看其日志。因此,为TCP服务器创建Windows服务并创建供人们运行以查看日志并在完成后关闭的GUI应用程序似乎是正确的选择。
\ $ \ endgroup \ $
–ŞafakGür
2012年10月24日在6:31

#2 楼

在不检查其他方面的情况下,我认为您应该分离代码,以便服务器本身在其类中(或者甚至更好的是在其自己的项目中)以及UI。例如,您不应在服务器代码中显示MessageBox。造成这种情况的原因有很多,因此我将仅保留此语句。换句话说,服务器应表现为黑盒,返回错误代码或引发异常,或其他任何行为,根据其自己的API。将服务器的代码与UI混合使用不是一种好习惯。