本章主干知识点:
- 本章主要目的是了解 Http 协议,学习 Socket 不是本章的主要目的,因此不必纠结 Socket 的细节问题
- 获得一个静态页面,浏览器向服务器发送什么格式的报文,以及服务器会返回给浏览器什么格式的报文
- HttpHandler 是对浏览器做处理的类
- HTML 中什么样的内容会被提交给服务器
学前说明
- ASP.NET WebForm 与 ASP.NET MVC
- 这里不是讲 WebForm,而是 ASP.NET 核心
- 建议使用 Chrome 浏览器
- 为了突出问题的核心,操作数据库暂时没有采用三层架构,后期项目中再用
- 理解了核心,才能做出性能高漏洞少的系统
登录流程
打开如鹏论坛登录页面,填入用户名密码,点击【登录】按钮,浏览器将用户输入的用户名、密码发送给网站服务器,网站服务器让负责处理登录请求的服务器程序来处理这个登录请求,处理程序判断用户名、密码是否正确,然后将判断结果返回给浏览器。
使用工具查看发送、返回报文理解:上网就是从服务器向浏览器传送 HTML 格式描述的网页,每次请求都带回来新的页面;页面中的图片、JS、CSS 在单独的请求中;
注册
如鹏网注册时,通过浏览器控制台 Network 监控浏览器与如鹏服务器间的信息传递。
Headers - Form Data 中有 POST 方式提交的信息,vcode 是填入的验证码值。
Response 里面是服务器返回的信息。
如果验证码输入错误,将返回。
验证码也是通过 url 向服务器请求的。带上请求参数 s=0.364… 能保证获取新验证码。
在 Preview 可以查看返回的验证码。
打开如鹏首页
打开如鹏网首页,一共有 75 个 requests。
访问网站时浏览器最先请求 HTML 文件。
服务器返回 HTML 文件后,浏览器自上向下进行解析,缺什么请求什么。
解析 HTML 时最先请求的就是 里面的 CSS 和 JS 文件。
然后就是请求各种页面上的图片资源。
编写“控制台浏览器”
Socket 是进行网络编程的类,通过 Socket 可以在两台计算机之间进行网络通讯,QQ 软件和 QQ 服务器之间、浏览器和网站服务器之间都是 Socket 网络通讯。Socket 不是学习重点,能运行、理解即可。开发网站不会这样写,这里所有涉及 Socket 的都是“原理性代码”。
向服务器发出指令:
// 请求资源 协议
GET /index.html HTTP/1.1
// 服务器地址 端口
Host: 127.0.0.1:8080
(回车)
然后从服务器获取返回。代码如下,尝试改一下获取其他页面、JS、CSS。
搭建服务器
此处使用 Cassini 临时搭建服务器。
控制台浏览器完整代码:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
namespace SocketDemo
{
class Program
{
static void Main(string[] args)
{
// 在 VS2010 及以下,要用:new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
using (var socket = new Socket(SocketType.Stream, ProtocolType.Tcp)) // TCP、UDP
{
// 连接服务器。http 协议默认的端口号是80。每个服务器软件监听一个端口(别的软件就不能监听这个端口了),发送给这个端口的数据只会被这个服务器软件接收到
// 百度是 123.125.114.144:80
socket.Connect(new DnsEndPoint("127.0.0.1", 80));// Connect 可以直接传入 IPAddress
// 读写 socket 通讯数据的流
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamWriter writer = new StreamWriter(netStream))
{
// 每行指令都回车一下。相对于网站根目录的路径,不要写在服务器上的全路径。
writer.WriteLine("GET /index.html HTTP/1.1");
writer.WriteLine("Host:127.0.0.1:80");
// 空行回车,表示指令结束
writer.WriteLine();
}
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamReader reader = new StreamReader(netStream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
}
Console.ReadKey();
}
}
}
服务器协议 HTTP/1.1 状态码 200 OK 服务器用的是 Cassini/4.0.1.7 版 服务器日期 ASP.NET Version Cache-Control 缓存策略 Content-Type 返回类型 返回长度(字节) 连接状态:断开 HTML 正文 |
|
---|---|
获取到 HTML 正文后就可以通过正则等各种手段解析页面代码,完成自己的浏览器。
请求百度首页:
socket.Connect(new DnsEndPoint("123.125.114.144", 80));
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamWriter writer = new StreamWriter(netStream))
{
writer.WriteLine("GET /index.html HTTP/1.1");
writer.WriteLine("Host:123.125.114.144:80");
writer.WriteLine();
}
请求如鹏首页:
socket.Connect("www.rupeng.com", 80);
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamWriter writer = new StreamWriter(netStream))
{
writer.WriteLine("GET /index.shtml HTTP/1.1");
writer.WriteLine("Host:www.rupeng.com:80");
writer.WriteLine();
}
请求不存在的页面 rupeng.com/1.html 时:
浏览器是什么
浏览器就是一个 Socket 网络客户端,主要帮助用户请求网站服务器上的内容并且把服务器返回的内容渲染(绘制)为图形化内容。
通过“开发人员工具”查看:
用户在浏览器输入 http://www.rupeng.com/index.shtml ,浏览器向 DNS 服务器请求 www.rupeng.com 的 IP 地址,然后浏览器向 rupeng 服务器发出 Socket 请求“GET
/index.shtml HTTP/1.1”等,服务器把 index.shtml 的内容返回给浏览器,浏览器解析
HTML 内容绘制页面,遇到 img 则浏览器再次发出 Socket 请求获得图片内容,然后绘制图片,遇到 CSS、JS 文件等也同样如此。
在控制台 Headers 里面可以看到,浏览器请求时携带的信息更加完备。
我们也可以在 Socket 请求时进一步完善。
socket.Connect("www.rupeng.com", 80);
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamWriter writer = new StreamWriter(netStream))
{
writer.WriteLine("GET /index.shtml HTTP/1.1");
writer.WriteLine("Host:www.rupeng.com:80");
writer.WriteLine("Cache-Control:max-age=0");
writer.WriteLine("Cookie:ASP.NET_SessionId=......");
writer.WriteLine();
}
编写网站服务器
首先使用 Socket 启动监听:
Socket serverSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8080));
serverSocket.Listen(10);
调用 Socket.Accept() 阻塞等待用户请求,当一个用户请求过来后 Accept() 方法再返回一个新的 Socket 对象。每个 Socket 代表一个连接通道,两个 Socket 形象的比喻“宿舍楼喊人”。
通过上一节服务器返回的内容我们知道,服务器端首先要读取用户的输入,分析输入,然后返回给用户内容“HTTP/1.1 200 OK”然后是一个空行,然后是正文,然后关闭 socket。代码如下。
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
namespace SocketDemo
{
class Program
{
static void Main(string[] args)
{
// 宿舍大妈,男生要通过她去找女性朋友
var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
// 监听 8080 端口
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8080));
// 启动监听,挂起的连接队列的最大长度为 10
serverSocket.Listen(10);
// 循环处理请求,男生去女生宿舍楼找女性朋友
while (true)
{
// 等待有人请求。这个 Socket 则是男女生通讯的通道
using (Socket socket = serverSocket.Accept())
{
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamReader reader = new StreamReader(netStream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
// 浏览器发送了 writer.WriteLine();
if (line.Length <= 0)
{
// 遇到空行代表客户端发送结束,开始给浏览器返回内容
break;
}
}
}
using (var netStream = new NetworkStream(socket))
using (var writer = new StreamWriter(netStream))
{
writer.WriteLine("HTTP/1.1 200 OK");
// Http协议规定:服务器返回给浏览器的报文头和正文之间用一个空行分割
writer.WriteLine();
writer.WriteLine("<b>北京时间:</b>" + DateTime.Now);
}
}
}
}
}
}
浏览器请求http://127.0.0.1:8080/1.html
浏览器页面效果:
控制台服务器效果:
静态网站服务器
根据请求的页面,返回页面内容。
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
namespace SocketDemo
{
class Program
{
private const string ServerLocation = @"F:\OneDrive - nenu.edu.cn\Rupeng\Projects\jQueryDemo\jQueryDemo\";
static void Main(string[] args)
{
var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8080));
serverSocket.Listen(10);
while (true)
{
string firstLine;
using (Socket socket = serverSocket.Accept())
{
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamReader reader = new StreamReader(netStream))
{
firstLine = reader.ReadLine();
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
if (line.Length <= 0) { break; }
}
}
var url = Regex.Match(firstLine, "GET /(.+) HTTP/1\\.1").Groups[1].Value;
using (var netStream = new NetworkStream(socket))
using (var writer = new StreamWriter(netStream))
{
if (File.Exists(ServerLocation + url))
{
writer.WriteLine("HTTP/1.1 200 OK");
writer.WriteLine();
var html = File.ReadAllText(ServerLocation + url);
writer.WriteLine(html);
}
else
{
writer.WriteLine("HTTP/1.1 404 NOT FOUND");
writer.WriteLine();
writer.WriteLine("未找到");
}
}
}
}
}
}
}
请求效果:
控制台服务器效果:
动态网站内容
请求网址格式的标准:? 前为处理程序路径,? 后为请求的参数,以 & 分割多个参数。
下面代码可以实现
- /add?i=1&j=2,运算 i + j
- /login?username=admin&password=123 登录
static void Main(string[] args)
{
var serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, 8080));
serverSocket.Listen(10);
while (true)
{
using (Socket socket = serverSocket.Accept())
{
string firstLine;
using (NetworkStream netStream = new NetworkStream(socket))
using (StreamReader reader = new StreamReader(netStream))
{
firstLine = reader.ReadLine();
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
if (line.Length <= 0)
{
break;
}
}
}
// add?i=1&j=2
var match = Regex.Match(firstLine, "GET /(.+)\\?(.+) HTTP/1\\.1");
var path = match.Groups[1].Value;
var queryString = match.Groups[2].Value;
var qsDict = ParseQueryString(queryString);
using (var netStream = new NetworkStream(socket))
using (var writer = new StreamWriter(netStream))
{
if (path == "add")
{
writer.WriteLine("HTTP/1.1 200 OK");
writer.WriteLine();
writer.WriteLine("result = " + (Convert.ToInt32(qsDict["i"]) + Convert.ToInt32(qsDict["j"])));
}
else if (path == "login")
{
writer.WriteLine("HTTP/1.1 200 OK");
writer.WriteLine();
if (qsDict["username"] == "admin" && qsDict["password"] == "123")
{
writer.WriteLine("Login successful!");
}
else
{
writer.WriteLine("Login failed!");
}
}
else
{
writer.WriteLine("HTTP/1.1 404 NOT FOUND");
writer.WriteLine();
writer.WriteLine("未找到");
}
}
}
}
}
private static Dictionary<string, string> ParseQueryString(string querystring)
{
...详见上例
}
请求处理响应
结合上面的例子,理解记忆此图。
Q:服务器知道浏览器什么时候关闭吗?
A:不知道。除非浏览器请求,否则服务器无法主动向浏览器发送数据!浏览器和服务器之间是短暂的网络连接。(网游是长期的)
Web 服务器和 ASP.NET
之前只是讲原理,不用深究。一般不会自己用 socket 从头写网站、太累、并发性、安全性都很麻烦。
现成的 Web 服务器:Apache、Nginx、IIS(.NET )。帮我们完成 Socket 基本的请求、静态文件的处理、并发性、安全性等。
后面再讲 IIS 的配置和使用,先使用
Cassini 这个 Mini 的网站服务器演示,把静态文件部署为网站。
建个“空 Web 应用程序”,创建一个 ashx,然后访问。ASP.NET 帮我们简化网站逻辑的编写,程序员只要写 ASP.NET 程序即可。
- 静态内容
- Web 服务器帮助处理静态文件请求
- 动态内容
- Web 服务器帮助进行 http 层面的处理(报文头等)
- ASP.NET 程序负责具体请求的处理(计算 i + j 等)
Test1.ashx 部分代码:
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
var i = Convert.ToInt32(context.Request["i"]);
var j = Convert.ToInt32(context.Request["j"]);
context.Response.Write(i + j);
}
浏览器效果:
HttpHandler(ashx)
ashx 是实现了 IHttpHandler 接口的类。
Test1.ashx.cs
namespace Web1
{
public class Test1 : IHttpHandler
{
/// <summary>
/// 处理请求方法
/// 用户访问 Test1.ashx 时,服务器调用 Test1 的 ProcessRequest 方法
/// </summary>
/// <param name="context">
/// context.Request 获取请求内容
/// context.Response 设置响应内容
/// </param>
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/plain";
var i = Convert.ToInt32(context.Request["i"]);
var j = Convert.ToInt32(context.Request["j"]);
context.Response.Write(i + j);
}
public bool IsReusable => false;
}
}
Test1.ashx 由两个文件 Test1.ashx 和 Test1.ashx.cs 组成,Test1.ashx 里面只有一行代码。
<%@ WebHandler Language="C#" CodeBehind="Test1.ashx.cs" Class="Web1.Test1" %>
- CodeBehind 指定了在 VS 里面双击 ashx 打开那个文件
- Class 指定了实际处理代码的位置
IIS Express
VS 项目 - 属性 - Web 里面可以设置使用 IIS Express(简化版)。
IIS Express 适合用于开发,拥有 IIS 完整的开发时的常用功能。
IIS 默认禁止浏览服务器下的文件,所以运行时会看到下面的界面。
调试器 - 取消勾选启用”编辑并继续“,可以实现中断调试修改代码的同时 IIS Express 继续在后台运行,C# 代码修改完后重新生成解决方案,刷新网页即可查看新效果。
HttpHandler 案例
案例:登录功能,登录成功显示 Hello,否则显示“登录失败”的图片页面。
public void ProcessRequest(HttpContext context)
{
// text/plain 纯文本,text/html HTML
context.Response.ContentType = "text/html";
var name = context.Request["name"];
var pwd = context.Request["password"];
context.Response.Write("<html><head></head><body>");
if (name == "admin" && pwd == "123")
{
context.Response.Write("<h1>Hello</h1>");
}
else
{
context.Response.Write("<img src='海错图.jpg'/>");
}
context.Response.Write("</body></html>");
}
浏览器控制台 Network 里面可以看到浏览器请求的各类型文件的 Content-Type。
表单提交
表单