您的位置:首页 > 编程语言 > ASP

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

2011-08-25 01:16 841 查看
在上篇简单的介绍了一下多线程的工作流程。
其实根据上一篇的讲解大家完全可以写一个聊天工具,类似QQ。大家可以写简单一点。
思路:
1,服务器与客户端通信,跟客户端与服务端通信一样,大家可以先实现服务端对客户端发消息。
2,客户端与客户端通信,就是客户端先把发送消息给服务器,里面带上对方客户端的ip和端口。
然后服务端将消息发送给对方客户端。
这里我就不去写了。写起来比较麻烦,大家可以去51cto下载源码,好像是“基于scoket的聊天工

具”吧。

现在开始写web服务器,深入底层的了解浏览器与服务器通信的原理。

写web服务器就不需要客户端了,客户端就是浏览器。
下面我们看看浏览器与服务器通信到底发送的是什么。
我们打开浏览器,下载httpwatch,打开httpwatch。开始记录浏览器发送的请求,和接受到的数据。如果不会使用的建议去网上查一些资料。

点击-record-打开网页(例如京东商城)-点stream
下面是几张图片给大家看一下。



请求报文:



响应报文:



请求数据:
GET /default.aspx HTTP/1.1
《请求方式 路径 HTTP协议版本》
Accept: */*
《浏览器处理内容的类型》
Accept-Language: zh-CN
《浏览器当前的语言环境》
Accept-Encoding: gzip, deflate
《浏览器支持的压缩方式》
User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; SLCC2; .NET CLR 2.0.50727; .NET CLR

3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; Tablet PC 2.0; .NET4.0C; .NET4.0E)
《当前浏览器以及操作系统信息》
Host: localhost:3485
《请求的域名地址》
Connection: Keep-Alive
《长连接》
这里说下长连接和短连接。
所谓的短连接就是服务器在发送回客户端请求的数据的时候立即关闭tcp通道。这样节约资源。但是带来一个弊端。因为我们打开一个

网页像服务器请求的次数是很多的。每当遇到图片,css,js代码都会发一次请求。这样每次打开关闭就非常的耗时。
所有就有了长连接。长连接就是在服务器发送会数据后不会立即关闭连接通道。当浏览器没有再发送请求过来,服务器才会关闭tcp连

接通道。

响应报文:
HTTP/1.1 200 OK
《HTTP版本 响应的结果》
Server: ASP.NET Development Server/10.0.0.0
《服务器信息》
Date: Wed, 24 Aug 2011 15:27:06 GMT
《日期》
X-AspNet-Version: 4.0.30319
《.NET版本》
Cache-Control: private
《关于缓存的信息》
Content-Type: text/html; charset=utf-8
《发送过来的内容的类型》
Content-Length: 27885
《响应报文体的字节数》
Connection: Close
《断开连接》

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-

transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1"><title>

</title>...............

我们所需要的响应报文包括:HTTP版本 响应的结果,发送过来的内容的类型,响应报文体的字节数
也就是说我们的web服务器需要像客户端也就是浏览器发送这三条数据,再加上请求的内容。
那好,我们开始写服务器。
文本框txtIp,txtPort btnListen,txtShow

点击按钮,开始启动服务器,并监听浏览器的请求。

sokWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress address = IPAddress.Parse(txtIP.Text.Trim());
IPEndPoint endpoint = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
sokWatch.Bind(endpoint);
sokWatch.Listen(10);
//启动线程监听浏览器请求
threadWatch = new Thread(WatchConnect);
threadWatch.IsBackground = true;
threadWatch.Start();

bool isWatch=true;
void WatchConnect()
{
while (isWatch)
{
//获得浏览器发送来的连接请求,返回一个负责与该浏览器通信的套接字
Socket sokConnection = sokWatch.Accept();
txtShow.AppendText("浏览器:" + sokConnection.RemoteEndPoint.ToString() + ",连接成功

**********************");
}
}
我们将接受请求和发送报文的方法封装到一个类中。这样看起来方便。
创建一个类:ConnClient,因为我们这里类需要操作接受请求和发送报文,所以需要当前的通信套接字。
我们在这个类中定义一个字段。
Socket sokMsg;//通信套接字
然后在构造函数中为他赋值。我们在创建这个类的时候就把当前的通信套接字传递过来。
然后根据这个套接字我们创建一个线程用于监听客户端浏览器发送过来的数据。
Socket sokMsg;
Thread threadMsg;
bool isReceive = true;
public ConnClient(Socket sokMsg)
{
this.sokMsg = sokMsg;
this.threadMsg = new Thread(ReceiveMsg);
}

public void ReceiveMsg()
{
if (isReceive)
{
byte[] byteMsg = new byte[1024 * 1024];
int length= sokMsg.Receive(byteMsg);
string strMsg = System.Text.Encoding.UTF8.GetString(byteMsg, 0, length);
}

}
将接受过来的数据保存到strMsg中。
我们上面看了HTTP请求的数据,我们现在所需要的主要是第一条中的请求的路径和文件。
所以我们先来获得所请求的文件的路径。
先讲这个字符串用一个稀有字替换掉\R\N(换行),然后根据这个稀有字分割成数组,取数组的第一条数据,然后

把第一条数据按空格分割成数组,然后取第二条数据。
string[] strArr = strMsg.Replace("\r\n", "耲").Split('耲');
string path = strArr[0].Split(' ')[1];
例如像上面的请求,我们就分割得到/default.aspx

有使用过IIS的时候,我们都知道在配置一个IIS服务器的时候需要指定一个路径。引文根据这个你当前站点的根目

录加上我们上面分割出来的路径拼接就能得到文件的绝对路径了。
我们就将当前的网站放到这个IIS应用程序中。顺便还可以了解一下反射。
一般来说,web服务器接受到的请求类型有三种,一种是静态页,一种是图片,一种是动态页面。
那我们就根据请求文件的后缀名做出不同的响应。
首先得到这个请求文件的类型 。那我们写个方法。将这个路径传过去。
string fileExtension = System.IO.Path.GetExtension(path);//获得被请求页面的 文件后缀
然后我们开始判断这个后缀了。如果是图片格式就执行处理图片的方法,如果是静态页就执行处理静态页的方法,

如果是动态页面,就执行处理动态法官法。方法。
那我们想想,我们执行处理页面的方法需要传递什么东西,首先路径是肯定得,我们得读这个请求的文件,我们在

发送响应报文的时候需要发送:HTTP版本 响应的结果,发送过来的内容的类型,响应报文体的字节数
里面有个内容的类型,也就是说我们需要后缀名。
所以传递两个参数。
基本的框架如下。

Socket sokMsg;
Thread threadMsg;
bool isReceive = true;
public ConnClient(Socket sokMsg)
{
this.sokMsg = sokMsg;
this.threadMsg = new Thread(ReceiveMsg);
}

public void ReceiveMsg()
{
if (isReceive)
{
byte[] byteMsg = new byte[1024 * 1024];
int length= sokMsg.Receive(byteMsg);
string strMsg = System.Text.Encoding.UTF8.GetString(byteMsg, 0, length);
string[] strArr = strMsg.Replace("\r\n", "耲").Split('耲');
string path = strArr[0].Split(' ')[1];
}
}
public void ExcuteRequest(string path)
{
string fileExtension = System.IO.Path.GetExtension(path);//获得被请求页面的 文件后缀
switch (fileExtension)
{
case ".html":
case ".htm":
case ".css":
case ".js":
//调用处理 静态页面的方法
ExcuteStaticPage(path, fileExtension);
break;
case ".jpg":
//调用处理 图片的方法
ExcuteImg(path, fileExtension);
break;
case ".aspx":
case ".jsp":
case ".php":
//调用处理 动态页面的方法
ExcuteDymPage(path, fileExtension);
break;
}
}
public void ExcuteStaticPage(string filePath, string fileExtension)
{

}
public void ExcuteImg(string filePath, string fileExtension)
{

}
public void ExcuteDymPage(string filePath, string fileExtension)
{

}
ConnClient类的执行静态页面的方法:
public class ConnClient
{
Socket sokMsg;
Thread threadMsg;
bool isReceive = true;
public ConnClient(Socket sokMsg)
{
this.sokMsg = sokMsg;
this.threadMsg = new Thread(ReceiveMsg);
threadMsg.IsBackground = true;
threadMsg.Start();
}

#region 05,监听请求 +void ReceiveMsg()
public void ReceiveMsg()
{
if (isReceive)
{
byte[] byteMsg = new byte[1024 * 1024];
int length = sokMsg.Receive(byteMsg);
string strMsg = System.Text.Encoding.UTF8.GetString(byteMsg, 0, length);
string[] strArr = strMsg.Replace("\r\n", "耲").Split('耲');
string path = strArr[0].Split(' ')[1];
ExcuteRequest(path);
}
}
#endregion

#region 04,根据不同的请求文件的后缀做出不同的处理 +void ExcuteRequest(string path)
public void ExcuteRequest(string path)
{
string fileExtension = System.IO.Path.GetExtension(path);//获得被请求页面的 文件后缀
switch (fileExtension)
{
case ".html":
case ".htm":
case ".css":
case ".js":
//调用处理 静态页面的方法
ExcuteStaticPage(path, fileExtension);
break;
case ".jpg":
//调用处理 图片的方法
ExcuteImg(path, fileExtension);
break;
case ".aspx":
case ".jsp":
case ".php":
//调用处理 动态页面的方法
ExcuteDymPage(path, fileExtension);
break;
}
}
#endregion

#region 03,执行静态页面请求的方法 +void ExcuteStaticPage(string path, string fileExtension)
public void ExcuteStaticPage(string path, string fileExtension)
{
StringBuilder sb = new StringBuilder();
//获得请求文件的文件夹的物理路径
string dataDir = AppDomain.CurrentDomain.BaseDirectory;
if (dataDir.EndsWith(@"\bin\Debug\") || dataDir.EndsWith(@"\bin\Release\"))
{
dataDir = System.IO.Directory.GetParent(dataDir).Parent.Parent.FullName;
}
//获得请求文件的物理路径(绝对路径)
path = dataDir + path;
//读取静态页面的内容
string fileContent = System.IO.File.ReadAllText(path);
//获得 静态页面内容的 字节数组 -- 就是 响应体内容
byte[] fileArr = System.Text.Encoding.UTF8.GetBytes(fileContent);
//获得 响应报文头
string responseHeader = GetResponseHeader(fileArr.Length, fileExtension);
byte[] arrHeader = System.Text.Encoding.UTF8.GetBytes(responseHeader);
//发送 响应报文头 回浏览器
sokMsg.Send(arrHeader);
//发送 响应报文体 回浏览器
sokMsg.Send(fileArr);
}
#endregion

#region 02,得到响应报文头 +string GetResponseHeader(int length, string fileExtension)
/// <summary>
/// 得到响应报文头
/// </summary>
/// <param name="length"></param>
/// <param name="fileExtension"></param>
/// <returns></returns>
public string GetResponseHeader(int length, string fileExtension)
{
System.Text.StringBuilder sbHeader = new StringBuilder();
sbHeader.Append("HTTP/1.1 200 OK\r\n");
sbHeader.Append("Content-Length: " + length + "\r\n");
sbHeader.Append("Content-Type: " + GetResponseContentType(fileExtension) +

";charset=utf-8\r\n\r\n");
return sbHeader.ToString();
}
#endregion

#region 01,根据后缀名得到请求文件的类型 +string GetResponseContentType(string fileExtension)
/// <summary>
/// 得到文件的后缀名
/// </summary>
/// <param name="fileExtension"></param>
/// <returns></returns>
public string GetResponseContentType(string fileExtension)
{
switch (fileExtension)
{
case ".aspx":
case ".jsp":
case ".php":
case ".html":
case ".htm":
return "text/html";
case ".css":
return "text/plain";
case ".js":
return "text/javascript";
case ".jpg":
return "image/JPEG";
default:
return "text/html";
}
}
#endregion

public void ExcuteImg(string filePath, string fileExtension)
{

}
public void ExcuteDymPage(string filePath, string fileExtension)
{

}
}

主界面代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Net.Sockets;
using System.Net;
using System.Threading;

namespace web服务器1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
TextBox.CheckForIllegalCrossThreadCalls = false;
}

Socket sokWatch = null;
Thread threadWatch = null;
private void btnListen_Click(object sender, EventArgs e)
{
sokWatch = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress address = IPAddress.Parse(txtIp.Text.Trim());
IPEndPoint port = new IPEndPoint(address, int.Parse(txtPort.Text.Trim()));
sokWatch.Bind(port);
sokWatch.Listen(10);
//启动线程监听浏览器请求
threadWatch = new Thread(WatchConnect);
threadWatch.IsBackground = true;
threadWatch.Start();
txtShow.AppendText("服务器开启成功...");
}
bool isWatch=true;
void WatchConnect()
{
while (isWatch)
{
//获得浏览器发送来的连接请求,返回一个负责与该浏览器通信的套接字
Socket sokConnection = sokWatch.Accept();
txtShow.AppendText("浏览器:" + sokConnection.RemoteEndPoint.ToString() + ",连接成功

**********************");
ConnClient conn = new ConnClient(sokConnection);
}
}
}
}

对于上述判断文件后缀类型只列出了一部分的类型。
对于图片处理,其实很简单,得到文件的路径后,使用FileStream读取这个图片,将读取出来的数据放到字节数组

中,发送到浏览器。如果把上面的看懂了,图片的处理就没什么难度。

对于动态页面的请求。这就需要依靠.NET framework来执行了。这个做起来还真不是我能做出来的。

似乎我们上面还没有用到委托等技术,由于现在比较晚,我就不做多解释了,只是稍微的提醒一下。我们不是自己写了一个类吗?如果我想将在这个类中操作文本框,就需要用到委托,例如,当我们发送报文的时候,将报文显示到文本框中,当我们浏览器退出的时候(我们对 public void ReceiveMsg()这个方法try catch一下,当我们的浏览器退出的时候就会捕捉到异常,我们可以在捕捉到异常的时候再文本框中提示一下“...断开连接”)。

TextBox.CheckForIllegalCrossThreadCalls = false;别忘记这个。

其实还有许多细节上面的东西,大家自己去注意吧。现在时间太晚了,我就不说多了,明天还得奋斗。

下面给大家看一张IIS的执行过程图

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐