通常,我们借助浏览器(通常是 IE,FireFox 或者 Chrome)浏览网页,例如,我们在地址栏中输入 DebugLZQ 的博客网址 http://www.cnblogs.com/DebugLZQ/ ,回车之后,就会在浏览器的窗口中看到 Debug 的主页.
在这个简单的操作背后影藏了巨大的复杂性.
我们在地址栏中输入的内容称为通用资源标记符(Universal Resource Identifier,URI)它有很多种样式,在 web 中我们通常称为统一资源定位符(Uniform Resource Locator,URL)的形式,它的格式如下:
协议:// 主机 [. 端口号][绝对路径 [? 参数]]
在 http://www.cnblogs.com/DebugLZQ/ 中,http 表示协议名称; www.cnblogs.com 表示主机的地址;可选的端口号没有出现,那么,将使用 http 协议默认的端口号 80;绝对路径为 / DebugLZQ/; 在这个例子中没有参数出现.
在. NET 中,不管是 URI 还是 URL,都使用定义在 System 命名空间中得 URI 类来进行处理.对应上面的介绍,这个类定义了 5 个属性,分别对应 5 个组成部分,如下所示:
Scheme:协议的名称
Host:取得 URI 地址中得主机部分
Port:取得端口号
AbsolutePath:绝对路径部分
Query:URI 地址中得参数部分
下面的例子演示了地址中各个部分:
namespace URI 说明
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
输出结果如下:
{
classProgram
{
staticvoidMain(string[] args)
{
System.Uri DebugLZQAddress = newUri("http://www.cnblogs.com/DebugLZQ/");
Console.WriteLine("Scheme: {0}",DebugLZQAddress.Scheme );
Console.WriteLine("Host: {0}", DebugLZQAddress.Host );
Console.WriteLine("Port: {0}", DebugLZQAddress.Port );
Console.WriteLine("AbsolutePath: {0}", DebugLZQAddress.AbsolutePath );
Console.WriteLine("Query: {0}", DebugLZQAddress.Query );
}
}
}
其中绝对路径部分使用类似于 Unix 的文件目录的形式来描述服务器中得资源,这个绝对路径呗传送到服务器之后,在 Web 服务器上通常被称为虚拟路径.
我们在地址栏输入 URL 后,如何找到服务器呢?互联网上的主机千千万,我们要访问的服务器是互联网上数千万台服务器中得一台,它很可能远在地球的另一边.浏览器要找到服务器,需要提供服务器的网络地址.
在当前的 TCP/IP 协议下,所谓服务器的网络地址,就是一个 IP 地址,目前我们使用 IPv4 的地址,即 IP 协议第 4 个版本规定的地址,每个地址由四个字节共 32 位组成.理论上将,可以表示 4G 个网络地址.通常我们用远点分隔四个数字来表示一个地址,每个数字对应地址的一个字节,例如,微软的 IP 地址为:207.46.19.254,直接在地址栏中输入 http://207.46.19.254 也可以访问网页.
但是,这些数字实在很难让人记忆,人们更愿意通过一个有意义的名字来找到一台主机.在经历了短暂得互联网初期阶段之后,1983 年,保罗 · 莫卡派(Paul Mockapetris)发明了域名系统,这样,在互联网上,我们可以为 IP 地址起一个有意义的名字以方便找寻主机,这个名字成为域名.比如,微软 Web 服务器的域名为 www.microsoft.com ,这个名字对应实际 IP 地址为 207.46.19.254.
虽然这个名字很好记,但是只有这个名字并不能直接找到微软的 Web 服务器,必须建立起名字和 IP 地址之间的对应关系.这个工作由域名服务器 DNS(即 Domain Name Server)完成.DNS 服务器提供一个列私语分层的通讯录,允许用户通过域名来查找对应的地址,或者完成通过地址来查找对应的域名.通常情况下,互联网服务商已经为我们自动设置了 DNS 服务器,因此可以简单地通过 www.microsoft.com 域名找到微软的 Web 服务器.
找到服务器之后,需要将请求从我们的客户端传输到服务器,那么,两台计算机是如何通信的呢?他们如何才能理解彼此发送的数据呢?这就需要提到协议.
当浏览器寻找到 Web 服务器的地址之后,浏览器帮助我们把对服务器的请求转换为一系列参数发送给 Web 服务器.服务器受到浏览器发来的请求参数之后,将会分析这些数据,并进行处理.然后向浏览器回应处理的结果,也就是一些新的数据;这些数据通常是 html 网页或者图片.浏览器收到之后,解析这些数据,将它们呈现在浏览器的窗口中,这就是我们看到的网页.
在浏览器与 Web 服务器的对话中,需要使用双方都能够理解的语法规范进行通信,这种程序之间进行通信的语法规范,我们称之为协议.协议有许多种,根据国际标准化组织 ISO 的网络参考模型,程序与程序之间的通信可分为 7 层,从低到高依次为:物理层,数据链路层,网络层,传输层,会话层,表示层,应用层.每层都有自己对应的协议.比如,应用层之间的协议我们称之为应用层协议.不同的应用程序可能有着不同的应用层协议.同一层的协议也可能有很多种.
浏览器与 Web 服务器之间的协议是应用层协议,当前,我们主要遵循的协议为 HTTP/1.1.HTTP 协议是 Web 开发的基础,这是一个无状态的协议,客户机与服务器之间通过请求和相应完成一次会话(Session).每次会话中,通信双方发送的数据称为消息(Message),消息分两种:请求消息和回应消息.
消息的格式如图所示.
每个消息可能由三部分组成,第一部分为请求行或者回应的状态行,第二部分为消息的头部,第三部分为消息体部分.消息头部分和消息体部分使用一个空行进行分隔.
通常情况下,我们在客户端使用浏览器来访问服务器,浏览器软件帮助我们构造所有的请求消息.使用 Fiddler 软件,可以帮助我们检测到浏览器与服务器之间的通信内容,如图所示.
上图右上部为浏览器请求的内容,可以看到,第一行为请求行,请求的内容为:
GET http://www.microsoft.com/en-us/default.aspx HTTP/1.1
下面的连续 N 行为请求头部分,然后是一个空行,由于是 GET 请求,所以没有请求体部分.
图右下部为服务器回应的内容,第一行为回应的状态行,HTTP/1.1 200 OK 表示请求的内容可以找到,但是需要到另外的地址去取.下面的 15 行为回应的头部.一个空行分隔了回应的头部和回应体部分,回应体中为一个简单的 HTML 网页.
HTTP 协议定义了内容的格式,这是一个应用层的协议,应用层协议的内容需要通过传输层在浏览器和服务器之间传送,TCP/IP 协议是 ISO 网络参考模型的一种实现.在 TCP/IP 协议中,与网络程序员相关的主要有两层:传输层和应用层.
传输层协议负责解决数据传输问题,包括数据通行的可靠性问题.传输层依赖更底层的网络层来完成实际的数据传输,在 TCP/IP 网络协议中,负责可靠通信的传输层协议为 TCP 协议.而网络层一般用网络驱动来实现,普通的程序员不会涉及;在 TCP/IP 协议中,网络层的协议为 IP 协议.
应用层用于在特定的应用程序之间传输数据.HTTP 协议就是 TCP/IP 协议中专门用于浏览器与 Web 服务器之间通信的应用层协议.应用层协议依赖于传输层协议完成数据传输,传输层协议依赖于网络层协议王城数据传输,他们之间的关系如下图(浏览器与服务器之间网络通信的传输过程):
到这里,我们的准备理论超不读了,哦,还得再认识下 Socket.
在遥远的 Unix 时代,为了解决传输层的编程问题,从 4.2BSD Unix 开始,Unix 提供了类似于文件操作的网络操作方式 ----Socket.通过 Socket,程序员可以像文件一样通过打开,写入,读取,关闭等操作完成网络编程.这使得网络编程可以统一到文件操作之下.通过 Socket 帮助程序员解决网络传输层的问题,而系统中得网络系统负责处理网络内部的复杂操作,这样程序员就可以比较容易地编写网络应用程序.需要注意的是应用层的协议需要针对网络程序专门处理,Socket 不负责应用层的协议,仅仅负责传输层的协议.
当然网络毕竟不是简单的文件,所以,在使用 Socket 的时候,程序员还是需要设置一些网络相关的细节问题参数.
当通过 Socket 开发网络应用程序的时候,首先需要考虑所使用的网络类型,主要包括以下三个方面:
1)Socket 类型,使用网络协议的类别,如 IPv4 的类型为 PF_INET.
2)数据通信的类型,常见的数据报(SOCK_DGRAM),数据流(SOCK_STREAM).
3)使用的网络协议,比如:TCP 协议.
在同一个网络地址上,为了区分使用相同协议的不同应用程序,可以为不同的应用程序分配一个数字编号,这个编号称为网络端口号(port).端口号是一个两字节的证书,取值范围从 0~65535.IANA(Internet Assigned Number Authority,互联网地址分配机构)维护了一个端口分配列表,这些端口分三类,第一类的范围是 0~1023,称为众所周知的端口,由 IANA 进行控制和分配,由特定的网络程序使用,例如,TCP 协议使用 80 号端口来完成 HTTP 协议的传输.第二类的范围是 1024~49151,称为登记端口,这些端口不由 IANA 控制,但是 IANA 委会了一个登记的列表,如果没有在 IANA 登记的话,也不应该在程序中使用.但是大多数的系统中,在没有冲突的情况下,也可以有用户程序使用.第三类的范围是 49152~65535,称为动态或者似有端口号,这些端口可以由普通用户程序使用.
对于一个网络应用程序来说,通过地址,协议和端口号可以唯一地确定网络上的一个应用程序.其中地址和端口的组合称为端点(EndPoint).每个 Socket 需要绑定到一个端点上与其他端点进行通信.
在. NET 中,System.Net 命名空间提供了网络编程的大多数数据类型以及常用操作,其中常用的类型如下:
1)IPAddress 类用来表示一个 IP 地址.
2)IPEndPoint 类用来表示一个 IP 地址和一个端口号的组合,称为网络的端点.
3)System.Net.Sockets 命名空间中提供了基于 Socket 编程的数据类型.
4)Socket 类封装了 Socket 的操作.
常用的操作如下:
1)Listen:设置基于连接通信的 Socket 进入坚挺状态,并设置等待队列的长度.
2)Accept:等待一个新的连接,当新连接到达的时候,返回一个指针对新连接的 Socket 对象.通过新的 Socket 对象,可以与新连接通信.
3)Receive:通过 Socket 接受字节数据,保存到一个字节数组中,返回实际接受的字节数.
4)Send:通过 Socket 发送预先保存在字节数组中得数据.
DebugLZQ:吼吼,有了上面的基础,下面用代码演示如何通过 Socket 编程创建一个简单的 Web 服务器.必要说明:这个服务器通过 49152 号端口提供访问,向浏览器返回一个固定的静态网页.在这个解决方案中,请求的消息由浏览器生成,并发送到服务器,这个程序将简单地显示请求信息.回应的消息由服务器程序生成,通过 Socket 传输层返回给浏览器.
namespace 基于 Socket 的最简单 Web 服务器
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Net;//
usingSystem.Net.Sockets;//
string responseBody = "From Socket Server
{
classProgram
{
staticvoidMain(string[] args)
{
IPAddress address = IPAddress.Loopback;// 取得本机的 loopback 网络地址,即 127.0.0.1
IPEndPoint endPoint = newIPEndPoint(address, 49152);// 创建可访问的端点,49152 表示端口号,如果设置为 0,表示使用一个空闲的端口号
Socket socket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);// 创建 socket, 使用 IPv4 地址,数据通信类型为字节流,TCP 协议
socket.Bind(endPoint);// 将 socket 绑定到一个端点上
socket.Listen(10);// 设置连接队列的长度
Console.WriteLine("开始监听,端口号:{0}",endPoint.Port );
while(true)
{
Socket client = socket.Accept();// 开始监听,这个方法会阻塞线程的执行,直到接受到一个客户端的请求连接
Console.WriteLine(client.RemoteEndPoint);// 输出客户端的地址
byte[] buffer = newbyte[4096];// 准备读取客户端请求的数据,读取的数据将保存在一个数组中
int length = client.Receive(buffer, 4096, SocketFlags.None);// 接受数据
// 将请求数据翻译为 UTF-8
System.Text.Encoding utf8 = System.Text.Encoding.UTF8;
string requestString = utf8.GetString(buffer, 0, length);
Console.WriteLine(requestString);// 显示请求
// 回应的状态行
string statusLine = "HTTP/1.1 200 OK\r\n";
byte[] statusLineBytes = utf8.GetBytes(statusLine);
// 准备发送回客户端的网页
Hello world.
";
运行后,在浏览器的窗口中输入: http://localhost:49152/ ,浏览器中可以看到如下的显示结果.
byte[] responseBodyBytes = utf8.GetBytes(responseBody);
// 回应的头部
string responseHeader = string.Format("Content-Type:text/html;charset=UTF-8\r\nContent-Length:{0}\r\n",responseBody.Length);
byte[] responseHeaderBytes = utf8.GetBytes(responseHeader);
// 向客户端发送状态信息
client.Send(statusLineBytes);
// 向客户端发送回应头
client.Send(responseHeaderBytes);
// 头部与内容的分隔行
client.Send(newbyte[]{13,10});
// 向客户端发送内容部分
client.Send(responseBodyBytes);
// 断开与客户端的连接
client.Close();
if(Console.KeyAvailable)
break;
}
socket.Close();
}
}
}
在命令行中看到如下输出:
来源: http://www.jianshu.com/p/75ea1f8928e8