您的位置:首页 > 其它

Socket写的Web服务器——带详细图解

2017-03-06 21:38 369 查看
http://www.cnblogs.com/lcomplete/p/use-csharp-write-aspnet-web-server.html

——闲扯:  

    Socket是大家都很熟悉的.NET处理底层硬件通信的类。比如:物联网中的一个器件要与其他器件相通信,那就必须使用到Socket来实现。但是我对Socket的中文翻译很不满意:Socket的中文翻译是“套接字”。我请问一下各位读者朋友,我如果只告诉你“套接字”你会知道这是什么吗? Socket的英文含义是:“插座、开关”,但你能通过“套接字”知道Socket的原意吗?

       Socket就像一根电话线,连接通两端的电话。让电话可以实现通信。我们声明一个Socket对象从实例开始监听的那一刻开始,Socket就像一个电话插座一样,随时监听等待消息的传入,而我们建立连接就像把插头插在这个插座上一样,一插即可通讯。效果和寓意正如英文的原意:插座、开关相符。      

       很多的外国技术文献翻译过来很难让人想象到它原本的意思,这是最失败的地方。而且直接音译的“套接字”也很难跟读音['sɑːkɪt]的Socket联系起来,反而更像读音['tɑːɡɪt]的target.很多晦涩难懂的专业技术名词,你只要查看其英文原意,往往都会恍然大悟、醍醐灌顶。我不知道“前辈”们为何会这样翻译,我以为一个东西的翻译可以有更好的选择,最起码不能翻译的太偏、太晦涩,以至于我们这些后来人很难接受。

       我认为Socket译为“通信插座”更为恰当。我们设置一个Socket对象的实例开始监听,就像设置一个电话插座在那一样,谁拨我这个“IP地址和端口”,我就接通谁。我觉得Socket翻译成“套接字”相对于林语堂大师翻译的“humor:幽默,sofa:沙发”相比,太让人无法接受了。

      总结:我推荐大家尽量去读英文原文的技术资料,去英文编程的技术网站和论坛去看。本人英语6级,虽然没有考过托福、雅思之类的,但是感觉看懂这些英文资料还是比较容易。 这或许受益于本人考研究生时对英语系统的复习,英语几乎每一个单词都有它的来历,‘汉字靠形造词,英语靠音造词’这是导致东西方文化、思想的区别的根源,也是我对学习英语最深的体会。

——正文:

  我们用过了IIS服务器,也了解了IIS服务器的实现原理和机制(读者如果不清楚,可以跟着我写完这个模拟的服务器,相信你就会明白了)。那么我们能不能手写一个类似于IIS的Web服务器呢?注意哦!我们这里写的是web服务器,而服务器有多种:FTP服务器(文件服务器)、POP3服务器(邮箱服务器)等,不过我想底层也应该大同小异.

 开始:

1、首先新建一个空白的解决方案,命名为WebServer.注意图中红色箭头的说明。

 


 2、在解决方案中添加一个WinForm应用程序,命名为“WebServer”,新建一个Winform窗体,并将窗体重命名为:"ServerForm".



3、拖动控件,进行如下布局:



4、对控件进行重命名操作:参考如图中所示。(希望读者养成规范的、良好的重命名的习惯)



5、布局完毕,剩下就是写程序了。写程序之前,我们需要先分析一下我们写Web服务器的思路

我们的思路:

(1)、先建立一个负责监听的“电话插座”——Socket,这个“电话插座”以指定的“IP地址和端口”作为“电话号码”,随时等待接通每一个拨打此“号码”(连接到此IP和端口)的人(在这里是程序进程)的电话。

(2)、因为我们当前的电话插座需要处理很多通信,所以每接通一个"电话"(接收到连接到该IP和端口的请求),我们就复制一个“电话插座”单独为该“电话”服务。(在这里我们会用到多线程的知识。 )

(3)、电话拨通了,但是我们需要懂双方的语言。也就是双方需要说同一门语言,或最起码有一个共同的互相都能懂得的语言约定。这就是HTTP协议。那么我们的浏览器和服务器之间的HTTP协议是什么样子的呢?往下看。

6、HTTP的协议分为:请求报文协议和响应报文协议。而无论是请求报文还是响应报文,其标准格式都是:头(header)、体(content).如:请求头,请求体;响应头,响应体。    

    (1)、下面来看一下我们的请求协议的报文是什么样子的:我们熟知的网页对服务器的请求分为get请求和post请求。

     a、get请求图(没有“请求体”): (那么get请求的请求体到哪里去了呢?请读者思考一下,相信很容易就想出答案)

     


    b、post请求图(请求头和请求体都有):请注意请求头和请求体之间的空行。这是HTTP协议请求报文的约定。

    

    (2)、下面让我们来看一下响应协议的报文是什么样子的

     


7、了解了请求协议的报文和响应协议的报文整体格式之后,我们需要进一步分析里面的“有用”的内容。回顾上面的请求报文图我们发现:

在第一行中包含了,请求方法、请求资源地址。

     


     好了我们拿到对方请求的报文之后,就可以截取这些“有用”的内容(注意:这里并不是说其他内容没有用,我们只是模拟Web服务器的主要功能),将响应的请求资源,以“响应协议报文”的格式,发送过去。这样浏览器也就会自动解读你发送的数据,我们的Web服务器也就实现了!

 

8、源代码开始了:

首先是ServerForm窗体的代码:

//*************************************************************************
//
//File Name:            ServerForm.cs
//
//Tables:               Nothing
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace WebServer
{
public partial class ServerForm : Form
{
public ServerForm()
{
InitializeComponent();
CheckForIllegalCrossThreadCalls = false;
}
private void btnStart_Click(object sender, EventArgs e)
{
// 第一步,设置顶级的监听端口的Socket对象
Socket serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

// 准备Socket绑定方法的参数对象IPEndPoint
IPAddress ipAddress;

if (!IPAddress.TryParse(txtIP.Text.Trim(), out ipAddress)) // 判断当前的IP地址栏数据是否可正常转换为IP地址
{
return;
}
int port;
if (!int.TryParse(txtPort.Text.Trim(), out port))// 判断当前的Port是否能转换为数字
{
return;
}

IPEndPoint ipEndPoint = new IPEndPoint(ipAddress, port);
// 开始顶级Socket的绑定和监听
try
{
serverSocket.Bind(ipEndPoint);
serverSocket.Listen(10);
SetLogText("服务器已经开启...");
// 设置线程,进行连接Socket对象的处理
Thread thread=new Thread(Listen);
thread.IsBackground = true;// 必须设置成为后线程,后台线程在窗体关闭的时候,会自动结束自己线程运行
thread.Start(serverSocket);// 将监听的的顶级Socket对象作为参数传入线程委托中的函数里面去
}
catch (Exception ex)
{
// 捕获到异常
SetLogText("服务器已经开启,您无需重复开启!");
SetLogText("  >详细信息:\r\n   "+ex.Message);
}
}

// 设置处理每一次监听到的连接的方法
private void Listen(object o)
{
Socket serverSocket = o as Socket;
while (true)
{
// 将服务监听到的连接,转换成一个Socket对象,后面将使用该连接的Socket进行HTTP请求的接收和响应的处理。
Socket connSocket = serverSocket.Accept();
SetLogText(connSocket.RemoteEndPoint+":已建立连接!");
// 尝试进行HTTP请求的接收和处理
try
{
// 声明接收HTTP请求的二进制字节数组
// 将接收到的二进制字节存放到声明的二进制字节数组中去
byte[] buffer=new byte[1024*1024];
int realLen = connSocket.Receive(buffer);

// 如果接收到的HTTP请求是空的,则关闭当前连接的Socket对象,返回进行下一次连接的监听。
if (realLen <= 0)
{
// 礼貌地关闭该连接Socket对象
connSocket.Shutdown(SocketShutdown.Both);
connSocket.Close();
SetLogText(connSocket.RemoteEndPoint + ":0字节请求,当前连接已关闭!");
return;
}

// 如果接收到的HTTP请求是正常的,则进行HTTP请求报文的分析,并生成HTTP响应报文
string content = Encoding.UTF8.GetString(buffer,0,realLen); // 读取HTTP请求报文
SetLogText(content);// 将该请求报文记录到服务器日志中
// 将有用的报文信息转换成Request(请求)对象;
Request request=new Request(content);
// 分析请求报文,进行HTTP响应处理
RequestStaticOrDynamicPage(request.RawUrl,connSocket);
}
catch (Exception)
{
// 提示异常的发生,并跳出死循环
SetLogText("当前连接发生异常,请重启服务!");
// 一旦接收异常,关闭此次连接的Socket
connSocket.Close();

break;
}
}
}

/// <summary>
/// 判断请求的是动态页面还是静态页面,并分别针对,进行HTTP响应处理
/// </summary>
/// <param name="rawUrl"></param>
/// <param name="connsocket"></param>
private void RequestStaticOrDynamicPage(string rawUrl, Socket connsocket)
{
// 根据请求文件的后缀名进行判断
string ext = Path.GetExtension(rawUrl);
switch (ext)
{
case ".aspx":
case ".asp":
case ".php":
case ".jsp":
// 动态页面的处理 (挖坑,读者自己来把这里补充完整)
break;
default:
// 静态页面的处理
ProcessStaticPageRequest(rawUrl,connsocket);
break;
}
}

/// <summary>
/// 处理HTTP的静态页面请求
/// </summary>
/// <param name="rawUrl"></param>
/// <param name="connsocket"></param>
private void ProcessStaticPageRequest(string rawUrl,Socket connsocket)
{
// 拼接物理路径的字符串,检测当前物理路径的文件是否存在
// 注意 Path.Combine()方法中,第二个开始以后的参数,开头的 / 要去掉,否则拼接出来的路径将从后面的
// 以 / 的字符串开始进行拼接,也就是忽略掉, / 前面的拼接路径字符串
rawUrl = rawUrl.TrimStart('/');
string physicalPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,"web",rawUrl);

// 进行检测当前请求的文件是否存在
if (File.Exists(physicalPath))
{
// 文件存在,读取到文件流中,拼接到HTTP响应对象——Response中的“响应报文”中的响应体中。
using (FileStream fs=new FileStream(physicalPath,FileMode.Open))
{
// 声明存储文件流的二进制字节数组
// 将文件流读取到声明好的二进制字节数组中去
byte[] buffer=new byte[fs.Length];
fs.Read(buffer, 0, buffer.Length);

// 准备发送响应报文
string ext = Path.GetExtension(rawUrl);
Response response=new Response(200,buffer,ext);
// 发送响应报文,关闭当前Socket连接,注意在这里体现了HTTP协议的无状态根本原因
connsocket.Send(response.GetResponse());
SetLogText(connsocket.RemoteEndPoint+":已关闭连接.");
connsocket.Close();

}
}
else
{
// 404 页面不存在处理
// 埋坑,读者可以在这里设置一个专门提示的页面,提示用户当前访问资源不存在
}
}

/// <summary>
/// 设置日志文本框的记录方法
/// </summary>
/// <param name="msg"></param>

private void SetLogText(string msg)
{
txtLog.AppendText(msg + "\r\n");
}
}
}


Request对象的代码:

//*************************************************************************
//
//File Name:            Request.cs
//
//Tables:               Nothing
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
using System;

namespace WebServer
{
class Request
{
#region 私有属性
private string _rawUrl;
private string _method;

public string RawUrl
{
get { return _rawUrl; }
set { _rawUrl = value; }
}

public string Method
{
get { return _method; }
set { _method = value; }
}
#endregion
#region 构造函数-属性初始化器

public Request(string content)
{
// 按行分解请求报文
string[] lines = content.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
// 按空格分解请求报文中的第一行,并初始化该对象的两个属性
this.Method = lines[0].Split(' ')[0];
this.RawUrl = lines[0].Split(' ')[1];
}

#endregion
}
}


Response对象的代码:

using System.Collections.Generic;
using System.Text;

//*************************************************************************
//
//File Name:            Response.cs
//
//Tables:               Nothing
//
//Author:               GuoHenghai
//
//Create Date:          6/08/2013
//
//*************************************************************************
namespace WebServer
{
class Response
{

#region 私有字段、属性
private int _codeStatus;
private int _contentLength;
private string _contentType;
private byte[] _buffer;

public int CodeStatus
{
get { return _codeStatus; }
set { _codeStatus = value; }
}

public int ContentLength
{
get { return _contentLength; }
set { _contentLength = value; }
}

public string ContentType
{
get { return _contentType; }
set { _contentType = value; }
}

public byte[] Buffer
{
get { return _buffer; }
set { _buffer = value; }
}
#endregion

#region 构造函数——属性初始化器

public Response(int codeStatus,byte[] buffer,string ext)
{
FillCodeStaDic();
this.Buffer = buffer;
this.CodeStatus = codeStatus;
this.ContentLength = buffer.Length;
GetContentType(ext);
}
Dictionary<int,string> codeStatusDic=new Dictionary<int, string>();

/// <summary>
/// 填充状态码 字典
/// </summary>
private void FillCodeStaDic()
{
codeStatusDic[200] = "OK";
codeStatusDic[404] = "请求页面不存在!";
//...挖坑,读者可以在这里进行详细的补充
}

/// <summary>
/// 根据请求文件的后缀名,确定响应体的类型
/// </summary>
/// <param name="ext"></param>
void GetContentType(string ext)
{
switch (ext)
{
case ".css":
this.ContentType = "text/css";
break;
case ".gif":
this.ContentType = "image/gif";
break;
case ".ico":
this.ContentType = "image/x-icon";
break;
case ".jpe":
case ".jpeg":
case ".jpg":
this.ContentType = "image/jpeg";
break;
case "bmp":
this.ContentType = "image/bmp";
break;
case ".js":
this.ContentType = "application/x-javascript";
break;
case "stm":
case ".htm":
case ".html":
this.ContentType = "text/html";
break;
// ...挖坑,读者可以在这里进行详细的补充
}
}

/// <summary>
/// 拼接响应报文
/// </summary>
public byte[] GetResponse()
{
// 拼接响应报文头
StringBuilder sb=new StringBuilder();
sb.Append("HTTP/1.0 "+this.CodeStatus+" "+codeStatusDic[this.CodeStatus]+"\r\n");
sb.Append("Content-Type: "+this.ContentType+"\r\n");
sb.Append("Content-Length: "+this.ContentLength+"\r\n");
sb.Append("Server: ghhSever/1.0\r\n");
sb.Append("X-Powered-By: MannyGuo\r\n");// 大家可以模拟下面的响应报文进行添加,注意格式必须要一致(末尾换行)
sb.Append("\r\n");
// 构建响应报文头
byte[] header = Encoding.UTF8.GetBytes(sb.ToString());
// 构建响应报文体
byte[] content = this.Buffer;
// 装载响应报文
List<byte>bList=new List<byte>();
bList.AddRange(header);
bList.AddRange(content);

return bList.ToArray();
}

#region 响应报文分析
/*
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 337
Connection: keep-alive
Date: Sun, 09 Jun 2013 04:50:44 GMT
Server: Apache
X-Powered-By: PHP/5.2.5
Content-Encoding: gzip
Vary: Accept-Encoding
Age: 37928
Via: 1.0 fe91fd60a17845818d57d903e10536ce.cloudfront.net (CloudFront)
X-Cache: Hit from cloudfront
X-Amz-Cf-Id: WKYiDsukwM6go6_K9lF207F72tlhGB6Wv1wgRutHWslDdd_7MoUpdw==

50
*/

#endregion
#endregion
}
}


9、演示效果:

为了演示效果,我们需要在程序的debug目录下新建一个Web文件夹,里面放一个测试用的1.html

  


 运行我们自己手写的Web服务器,启动服务。在浏览器地址中输入“IP地址:端口号/页面(或者资源)”,就可以看到效果了。



10、上一篇文章,短短3天内浏览量超过了1000。小郭在此感谢大家的支持!我会一如既往的为大家奉献更多的东西。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: