自己动手写web服务器(上),深入底层了解ASP.NET浏览器与服务器通信原理

前几天看到51cto一个人法的一个基于Socket的聊天工具。
突然就自己写了一个简单的web服务器。
花了半天写下来一篇文档,特地过来分享(如有错字,请大家谅解)

上次有人说很佩服我用记事本写代码,其实也没什么,把代码写熟练了在哪里都能写。
首先这个服务器技术需要用到委托,多线程,socket通信。
委托,这里就随便的提一下,大家不懂的去别的地方找点资料学习一下。
一、委托
委托说白了就是一个安全的函数指针。也就是说我们可以把委托当成一个参数传递。
委托是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为。委

托方法的使用可以像其他任何方法一样,具有参数和返回值。
这里就不去多讲解,这不是我要说的重点。具体的用法大家可以在后面说服务器的时候了解到。

二、多线程
我们一般说每一个应用程序就是一个进程,一个进程可以中有多个线程。但是我们创建一个应用

程序的时候默认只有一个线程(单线程),也就是主线程。也称为UI线程。
那我们举一个很简单的例子来说说单线程带来的问题
在窗体上面放一个按钮,点击按钮触发一个事件,在这个事件中做一个循环
for(int i=0;i<9999999999;i++)
{

}
MessageBox.Show("循环完成");
我们在循环中不做任何事情。运行,我们点击按钮之前可以看到我们的窗体是可以移动的。当我

们点击按钮之后就不能移动了,当循环完之后我们的窗体又能移动。
也就是说当我们循环的时候我们的UI线程就去做循环去了,没有空闲去处理窗体的一个操作。
多线程的好处:让一个程序同时处理多个事情。
例如一个公司,事情变的太多的时候,老板就考虑去请一些人去帮他做事情。那么每一个人就是

一个线程。

多线程的操作很简单,就是一个类,Thread。
既然知道了是这个类,我们现在开始创建一个线程。

Thread thread=new Thread(
好,这个时候我们发现里面需要传一些我们不知道的数据类型,我们转到定义,看到构造函数中

有2种数据类型的参数
一个是ParameterizedThreadStart
另一个是ThreadStart
我们分别转到定义看,好,两个都是委托,前者要带object类型的参数,后者不带参数。都无返

回类型。

也就是说我们在new一个线程的时候需要传递一个方法(每一个人进公司肯定要给你一些任务,让

你做什么事情,总不能花钱请你过来让你玩)。
那上面的单线程所带来的问题就好解决了,我们将要执行的代码放到一个方法中,这个方法按照

上述两种委托的签名来定义。

因为我们不需要参数,所以使用第二种委托。
Thread thread = new Thread(Count);
thread.start();
public void Count()
{......}

这样就OK了。但是我们又会发现一个问题,当循环还没结束的时候我们点击X关闭窗口,但是主程

序并为退出,也就是我们的VS上面还带着一把锁。当循环完成后弹出那个对话框,我们点击确定

后主
程序才会退出。

这里就涉及到前台线程和后台线程。
前台线程:只有所有的前台线程都关闭才能完成程序关闭时。
后台线程:只要所有的前台线程结束,后台线程自动结束。
所以我们需要将这个线程设为后台线程
Thread thread = new Thread(Count);
thread.isbackground=true;
thread.start();

我们使用完不带参数的委托,那我们怎么使用带参数的委托呢 。
public void Count(object name)
{......}
Thread thread = new Thread(Count);
thread.isbackground=true;
thread.start();
很闲然这样是不行的,因为我们的方法需要参数,而我们并没有传递参数。
细心的其实可以看到start方法还有一个重载,可以传递一个object类型的参数。似乎想到了什么

。因为我们带参数的委托的参数也是object类型的。
其实正是这样,这个我想就不需要我继续说下去了吧。

三、Socket
Socket我们通常称为“套接字”,用于描述IP地址和端口,是一个通信链的句柄。其实说白了,

就是两个应用程序之间通信的一个工具。
就像你跟你把打电话,为什么你们能对话,手机就是Socket。
我们想象你爸那边是服务端,你这边是客户端。
1,首先你爸得有手机,手机有型号什么的,肯定不能是玩具手机。这个过程就是创建socket对象

,我们一般使用三个参数的那个构造函数。
Socket sokServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream,

ProtocolType.Tcp);指定socket的寻址方案,套接字类型(流式连接),协议类型TCP协议。

2,电话的电波怎么才能找到你爸的那部电话机,世界上那么多的手机,那么手你爸肯定有手机卡

,你根据手机卡的号码来与你爸爸通信。那么在网络上的应用程序,我们就需要指明IP和端口。

IP是为了找到你当前应用程序所在的电脑,端口是找到你这个应用程序在电脑中的位置。
IPAddress address = IPAddress.Parse(txtIP.Text);
IPEndPoint port = new IPEndPoint(address, int.Parse(txtPort.Text));
我服务器的窗体上有两个文本框一个按钮,txtIp(127.0.0.1), txtPort(8088),btnListen

3,有了手机,有了手机卡,那手机卡肯定得插到手机里面去。也就是说我们的socket需要绑定到

这个服务器的IP和端口。
sokServer.Bind(port);

4,卡插到手机上了。没开机怎么接电话。所以我们需要开机。
也就是监听客户端的请求
sokServer.Listen(10);这里的10是什么呢?像很多手机都能够同时接多个电话,这里的10就是说

我的手机能同时接10个电话。

5,好,你爸的手机全部准备完毕,就等你打电话。首先你这边是客户端,你的手机也是一个套接

字。
那我们先创建一个套接字。
Socket sokClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream,

ProtocolType.Tcp);

6,你跟你爸打电话,得指定就打你爸那号码,所以需要指定你要连接的服务器的IP和端口。
IPAddress address = IPAddress.Parse(txtIP.Text);
IPEndPoint port = new IPEndPoint(address, int.Parse(txtPort.Text));
txtIp(127.0.0.1), txtPort(8088)这个IP和端口是服务器那边的IP和端口。

7,现在你开始打电话了,也就是连接服务端。
sokClient.Connect(port);

8,现在你的电话打过去了,也就是连接请求发过去了。你爸那边的手机得接受吧,接受了还得与

你讲话。
你爸现在听到手机响了,把手机拿出来接电话.按下了接听键
也就是说服务端接收到客户端的请求
sokServer.Accept();
txtShow.appendText("接受连接成功......\r\n");

9,你跟你爸说:“爸我没钱了,给我打点钱”。

sokClient.send(),我们发现传递的是一个二进制数组,所以我们先定义一个数组,指定为1M
byte[] arrMsg=new byte[1024*1024]
将我们要发送的数据保存到数组里面去。本来我们是要发送字符串的,现在定义了一个字节数组

,规定了长度,但是我们把字符串转为字节肯定不会刚好等于1024*1024,所以我们在new的时候

就直接将字符串转为字节数组,
byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(txtInput.Text.Trim());
sokClient.Send(arrMsg);
10,你爸要接收到消息,然后与你通话。
也就是说服务器端要接收客户端的请求
sokServer.Accept();
txtShow.appendText("接收到服务器端请求")。
byte[] msgArr = new byte[1024 * 1024 ];//接收到的消息的缓冲区
int length=0;
//接收服务端发送来的消息数据
length =sokServer.Receive(msgArr);//Receive会阻断线程
string strMsg = System.Text.Encoding.UTF8.GetString(msgArr, 1, length - 1);
txtShow.AppendText(strMsg + "\r\n");

11,当你爸听到你的消息,那你爸要跟你说话啊。客户端与浏览器通信,客户端是用套接字通信

的,服务器端也是用套接字进行接收。但是如果服务器要与客户端通信必须要再创建一个套接字

,专门负责与这个客户端通信。因为一个服务器会有多个客户端来访问。就像你爸的手机可能有

多个人打电话过来,你必须要与每一个打给你电话的人都进行通信。我们知道创建一个套接字都

要制定IP和端口,也就是说要指定要与谁通信。你总不能要你爸的一个手机只能与你一个人通信

吧。那你爸不得买几百个手机。
在服务器端肯定不会知道你要与哪一个客户端通信。这个也不需要我们考虑。我们可以看到

Accept方法返回的是一个套接字。也就是说:当你的客户端向服务器进行通信的时候,服务器端

会自动创建一个套接字,这个套接字专门负责与你通信。
Socket sokWatch = sokServer.Accept();那我们用一个套接字来接受,这个套接字就专门负责与

这个发送消息过来的客户端进行通信。
12,但是Accept方法会阻断当前线程的运行,这样会给用户很不好的感觉。所以我们需要创建一

个线程来专门接受客户端的请求。我们创建一个监听的方法。但是大家想想,这个方法只能执行

一次的,但是我们的服务端随时都会有客户端发送过来消息。所以我们要时时刻刻监听。所以要

做一个循环。定义一个全局的bool变量标识是否监听。赋值为true。
bool isWatch = true;
 public void Watch()
        {
            while (isWatch)
            {
                Socket sokWatch = sokServer.Accept();
                txtShow.AppendText(sokWatch.RemoteEndPoint.ToString());
            }
        }
sokWatch.RemoteEndPoint.ToString()获得客户端的IP和端口。

当我们自己创建的线程去操作界面的元素的时候是不允许的。就像公司的销售部门的想来C#代码

,那肯定是不行的,必须要通过一个检验。看你是否有实力。
所我们要对该窗体取消对跨线程调用界面控件的检查。
放在构造函数中。
TextBox.CheckForIllegalCrossThreadCalls = false;//关闭跨线程修改控件检查

这样我们就循环监听客户端的的请求

下面是全部的代码。

服务器端代码:
public FormServer()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }

        Thread thread =null;
        Socket sokServer;
        private void btnListen_Click(object sender, EventArgs e)
        {
            //创建服务端的socket
            sokServer = new Socket(AddressFamily.InterNetwork, SocketType.Stream,

ProtocolType.Tcp);
            IPAddress address = IPAddress.Parse(txtIP.Text);
            IPEndPoint port = new IPEndPoint(address, int.Parse(txtPort.Text));
            // 端口和Ip绑定到sokcet中
            sokServer.Bind(port);
            //开始监听客户端的请求
            sokServer.Listen(10);
            //由于Accept接受请求的方法会阻断当前线程,所以创建一个新线程
            thread = new Thread(Watch);
            //设置为后台线程
            thread.IsBackground = true;
            //开启线程
            thread.Start();
            txtShow.AppendText("服务器开启成功");
          
        }
        bool isWatch = true;
        public void Watch()
        {
            //能与多客户端通信多次
            while (isWatch)
            {
                //当接受到客户端发送过来的请求的时候立即创建一个套接字专门负责与它通

信。
                Socket sokWatch = sokServer.Accept();
                //获得客户端的IP和端口
                txtShow.AppendText(sokWatch.RemoteEndPoint.ToString());
                byte[] msgArr=new byte[1024*1024];
                //将客户端发送过来的数据,保存到字节数组中,因为发送的数据都是字节数


                //接受到的数据的长度,因为上面创建的字节数组是1024*1024,但是发送的数

据一般都没这么大,所以我们将字节数组转换成字符串的时候只需要转从0到length的数据。
                int length = sokWatch.Receive(msgArr);
                string strMsg = System.Text.Encoding.UTF8.GetString(msgArr, 0,

length);
                txtShow.AppendText(strMsg + "\r\n");
            }
        }


客服端代码:
        public FormClient()
        {
            InitializeComponent();
            TextBox.CheckForIllegalCrossThreadCalls = false;
        }
        Socket sokClient = null;
        private void btnConn_Click(object sender, EventArgs e)
        {
            sokClient=new Socket(AddressFamily.InterNetwork, SocketType.Stream,

ProtocolType.Tcp);
            IPAddress address = IPAddress.Parse(txtIp.Text);
            IPEndPoint port = new IPEndPoint(address, int.Parse(txtPort.Text));
            sokClient.Connect(port);
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            byte[] arrMsg = System.Text.Encoding.UTF8.GetBytes(txtMsg.Text);
            sokClient.Send(arrMsg);
        }

你可能感兴趣的:(asp.net)