网络 - 笔记
主要命名空间是 System.Net.*
和 System.IO.*
主要内容:
HttpClient
来发送 HTTP 请求HttpListener
来实现 HTTP 服务器SmtpClient
来处理邮件Dns
来转换域名与地址TcpClient
,UdpClient
,TcpListener
, 和Socket
来直接访问转换层与传输层.
处理
FTP
推荐使用 Nuget 包FluentFTP
.
网络架构
下图是基本的网络架构 (细节略)
然后是一个网络术语(缩写术语)表
简写 | 含义 | 备注 |
---|---|---|
DNS | Domain Name Service | 在域名与 IP 地址之间转换. |
FTP | File Transfer Protocol | 网络上收发文件的协议. |
HTTP | Hypertext Transfer Protocol | 呈现 Web 页面, 运行 web 服务. |
IIS | Internet Information Services | |
IP | Internet Protocol | 网络层协议, 在 TCP, UDP 之下. |
LAN | Local Area Network | |
POP | Post Office Protocol | |
REST | ||
SMTP | ||
TCP | ||
UDP | ||
UNC | ||
URI | ||
URL |
地址与端口
介绍 IPv4 和 IPv6 的概念. 重点是怎么表示 EndPoint
.
System.Net
命名空间下的 IPAddress
类专门用于描述地址. 结合 IPEndPoint
来, 使用端口后, 可以表示完整的主机地址信息.
var a = IPAddress.Parse("192.168.1.123");
var ep = new IPEndPoint(a, 8080);
URIs
介绍 URI 的结构与基本用法
常用的方式:
- 构建安全的 URL, 处理路径合并等信息
- 解析 URL, 可以使用属性来访问各个部分
HttpClient
比较经典的类, 用于完成基本的网络处理, 主要用于替换老的 WebClient
, WebRequest
, 以及 WebResponse
.
简单说就是比较上层的请求处理类, 方便使用, 支持并发, 适用于单元测试与简单的请求处理. 但是不支持请求过程中的进度控制.
该类提供丰富 Get*
系列方法, 可以完成诸多请求:
var res = await new HttpClient().GetStringAsync("http://...");
还有很多系统方法, 其中方法都是异步的.
不同于老的 Web...
类, HttpClient
的实例可以被复用, 允许同时请求, 它是异步的.
HttpClient
的属性比较少, 常用的就 BaseAddress
, Timeout
等. 其他的属性配置需要借助于另一个类: HttpClientHandler
.
HttpClient
有点类似于剥壳, 其仅提供基础功能, 它联系了实际上工作的对象.
var handler = new HttpClientHandler() { UseProxy = false };
var client = new HttpClient(handler);
对于代理, cookie, 重定向, 鉴权等.
GetAsync 和 响应消息
GetStringAsync()
, GetByteArrayAsync()
, GetStreamAsync()
都是对 GetAsync()
的封装. 底层为 GetAsync()
方法. 该方法会返回一个响应消息.
var client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("http://...");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();
使用 HttpResponseMessage
可以获得响应状态码与响应头等信息. 默认情况下, 未成功的响应不会引发异常, 除非主动调用 response.EnsureSuccessStatusCode()
方法.
有一个 CopyToAsync()
方法, 可以方便的将响应体写入流中.
using var stream = File.Create("text.html");
await response.Content.CopyToAsync(stream);
除了 GetAsync()
之外, 还有 PostAsync()
, PutAsync()
, 以及 DeleteAsync()
.
SendAsync 和 请求消息
比起 GetAsync()
更底层的方法是 SendAsync()
. 要使用它, 需要使用 HttpRequestMessage
类.
var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "http://...");
var response = await client.SendAsync(request);
...
使用该方式可以自定义更多请求的细节.
上传数据与 HttpContent
有了 HttpRequestMesage
后, 可以为 Content
属性赋值来上传数据. Content
的类型是 HttpContent
, 它是一个抽象类. 可以使用的内置类型有 (支持自定义):
ByteArrayContent
StringContent
FormUrlEncodedContent
(用于表单提交数据)StreamContent
例如:
...
request.Content = new StringContent("This is a test");
...
HttpMessageHandler
之前描述过, HttpClient
只是一个壳, 具体的配置在 HttpClientHandler
中, 而 HttpClientHandler
又是 HttpMessageHandler
的子类. 它也是一个抽象类:
public abstract class HttpMessageHandler: IDisposable {
protected internal abstract Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken);
public void Dispose();
protected virtual void Dispose (bool disposing);
}
而 HttpClient
中调用的 SendAsync
, 就是这个方法.
由于 HttpMessageHandler
足够简单, 它更容易进行扩展, 完成既定任务.
单元测试与模拟数据
可以对 HttpMessageHandler
进行派生来 mocking
数据, 来辅助单元测试
class MockHandler: HttpMessageHandler {
Func<HttpRequestMessage, HttpResponseMessage> _responseGenerator;
public MockHandler(
Func<HttpRequestMessage, HttpResponseMessage> responseGenerator
) {
_responseGenerator = responseGenerator;
}
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
) {
cancellationToken.ThrowIfCancellationRequested();
var response = _responseGenerator(request);
response.RequestMessage = request;
return Task.FromResult(response);
}
}
这个方法定义了如何请求与响应, 用这个方法可以预定义响应结果, 然后基于该结果来进行断言, 完成单元测试. 因为如何响应是在构造函数中定义的, 由使用者定义, 这样可以应用的场景更多.
var mocker = new MockHandler(request => new HttpResponseMessage (HttpStatusCode.OK) {
Content = new StringContent("...")
});
var client = new HttpClient(mocker);
...
使用 DelegatingHandler 来处理链式处理
可以派生 DelegatingHandler
, 然后在消息处理中调用另一个处理, 形成一个链式处理. 使用这个方法可以在内部实现自定义鉴权, 压缩, 以及自定义加密协议等. 下面是一个日志处理实例:
class LoggingHandler: DelegatingHandler {
public LoggingHandler(HttpRequestHandler nextHandler) {
InnerHandler = nextHandler;
}
public async override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken
) {
Console.WriteLine("请求 " + request.RequestUri);
var response = await base.SendAsync(request, cancellationToken);
Console.WriteLine("获得响应 " + response.StatusCode);
return response;
}
}
这里重写后又使用 async
修饰是合法的.
这里还有一个更好的办法, 就是不去写死 Console
, 而是利用构造器传入 Action<T>
来进行日志的输出, 做到可定制输出方式.
代理 (Proxy)
首先作者简要介绍了一下代理服务器的含义与作用.
- 它是一个中间服务
- HTTP 请求会被路由到这里
常常在企业中会指定部分人员可访问某特定环境, 可以通过代理服务器来完成.
计算网络还是需要弄一下.
基本用法:
创建
WebProxy
实例, 必要时配置鉴权.创建
HttpClientHandler
, 配置proxy
.创建
HttpClient
使用对应HttpClientHandler
即可.
var proxy = new WebProxy("192.168.10.49", 808);
proxy.Credentials = new NetworkCredential("username", "password", "domain");
var handler = new HttpClientHandler() { Proxy = proxy };
var client = new HttpClient(handler);
...
如果需要禁用代理, 可以设置 handler.UseProxy = false
, 不需要清空 Proxy
属性.
如果在 NetworkCredential
中设置了域, 那么会使用基于 win 的鉴权方案. 要使用当前用户, 则为代理的 Credentials
属性设置静态值 CredentialCache.DefaultNetworkCredentials
.
为了避免反复设置 Proxy
属性, 有一个全局配置方法:
HttpClient.DefaultWebProxy = myWebProxy;
鉴权
可以按照下面方法为 HttpClient
提供用户名与密码:
string username = "myuser";
string password = "mypassword";
var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential(username, password);
var client = new HttpClient(handler);
...
这部分逻辑暂略, 不太熟悉关于这部分背后密码控制的流程
CredentialCache
略
使用请求头处理鉴权
直接在请求头中设置:
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthorizationHeaderValue(
"Basic",
Convert.ToBase64(Encoding.UTF8.GetBytes("username:password"))
);
...
该策略对基于请求头的鉴权均有效, 例如 OAuth 等.
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthorizationHeaderValue("Bearer", token);
...
Header
作者简要介绍了一下 Header 的结构, 它就是键值对, 以及提示了 .NET 提供了标准的 Header 集合可使用. 该集合提供强类型的支持, 也可以使用自定义头.
只需要通过 DefaultRequestHeaders
属性即可访问.
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("VS", "2022")
);
client.DefaultRequestHeaders.Add("CustomHeader", "VS22");
HttpClient
的 DefaultRequestHeaders
属性会作用域所有的请求; 如果需要单个请求控制 Header, 可以使用 HttpRequestMessage
的 Headers
属性.
QueryString
重点是对参数值进行序列化. 主要是 Uri
提供的静态方法 EscapeDataString()
, 对于 URL 依旧是字符串拼接.
还有一个
EscapeUriString
方法.
上传数据
主要是模拟表单请求. 涉及到的类 FormUrlEncodedContent
, 该类将键值对封装成表单数据.
...
var dic = new Dictionary<string, string> { ... };
var values = new FormUrlEncodedContent(dic);
var response = await client.PostAsync(uri, values);
...
Cookies
简要介绍了 Cookie 的结构, 即键值对. 不过 每一个 Cookie 值也是带有属性的, 涉及到可访问性, 过期时间等.
然后介绍了 Cookie 的作用, 就是放服务器知道是同一个客户端访问了.
默认 HttpClient
会忽略从服务器返回的 Cookie 数据. 要使用 Cookie, 需要创建 CookieContainer
对象.
var cc = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cc;
var client = new HttpClient(handler);
手动往 Cookie 中添加数据:
var c = new Cookie("Key", "value", ...);
cc.Add(c);
编写 Http 服务
基于 .NET 6 编写 HTTP 服务, 使用 Top-Level 的最佳方法是使用 minimal API.
var app = WebApplication.CreateBuilder().Build();
app.MapGet("/", () => "Hello World");
app.Run();
也可以使用 HttpListener
来实现. 这是一个底层方法. 下面是一个案例, 监听 51111 端口, 监听单个固定地址, 请求后响应一行消息:
using var server = new SimpleHttpServer();
// 请求
Console.WriteLine(
await new HttpClient()
.GetStringAsync("http://localhost:51111/MyApp/Request.txt")
);
class SimpleHttpServer {
readonly HttpListener listener = new HttpListener();
public SimpleHttpServer() => ListenAsync();
async void ListenAsync() {
listener.Prefixes.Add("http://localhost:51111/MyApp/");
listener.Start();
// 等待客户端请求
HttpListenerContext context = await listener.GetContextAsync();
// 响应请求
string msg = "你请求的是 " + content.Request.RawUrl;
context.Response.ContentLength64 = Encoding.UTF8.GetByteCount(msg);
content.Response.StatusCode = (int)HttpStatusCode.OK;
using Stream s = context.Response.OutputStream;
using StreamWriter writer = new StreamWriter(s);
await writer.WriteAsync(msg);
}
public void Dispose() => listener.Close();
}
一次性请求.
在 Win 中 HttpListener
内部没有使用 Socket
对象, 而是使用的 Windows HTTP Server API (系统调用). 这一特点允许同一个 IP, 同一个端口, 监听不同的地址. 对特殊的企业防火墙的配置有一定优势 (不用单独开启端口).
然后作者对代码进行了简要解释 (监听, 阻塞, 请求, 响应等), 并对请求处理过程做了介绍.
要处理完整的 HTTP 请求, 至少要写入 Content-Length
和 StatusCode
.
下面是一个简单的 web page server, 采用异步的方式:
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
class WebServer {
HttpListener _listener;
string _baseFolder; // web 页面所在目录
public WebServer(string uriPrefix, string baseFolder) {
_listener = new HttpListener();
_listener.Prefixes.Add(uriPrefix);
_baseFolder = baseFolder;
}
public async void Start() {
_listener.Start(); // 开启, 然后进入循环, 开始监听, 阻塞
while (true) {
try {
var context = await _listener.GetContextAsync();
Task.Run(() => ProcessRequestAsync(context));
}
catch(HttpListenerException) { break; } // 停止监听
catch(InvalidOperationException) { break; } // 停止监听
}
}
public void Stop() => _listener.Stop();
async void ProcessRequestAsync(HttpListenerContext context) {
try {
string fileName = Path.GetFileName(context.Request.RawUrl);
string path = Path.Combine(_baseFolder, fileName);
byte[] msg;
if (!FIle.Exist(path)) {
Console.WriteLine("资源未找到: " + path);
context.Response.StatusCode = (int)HttpStatusCode.NotFound;
msg = Encoding.UTF8.GetBytes("Sorry, that page dose not exist");
} else {
constext.Response.StatusCode = (int)HttpStatusCode.Ok;
msg = File.ReadAllBytes(path);
}
context.Response.ContentLength64 = msg.Length;
using (Stream s = context.Response.OutputStream)
await s.WriteAsync(msg, 0, msg.Length);
} catch (Exception ex) {
Console.WriteLine("请求错误: " + ex);
}
}
}
Tips
需要注意的是, 如果端口被占用的情况下, HttpListener
无法启动, 除非占用端口的也采用 Windows HTTP Server API.
为了提升性能, 这里使用了异步方法. 如果是在 UI 开发中, 可能会阻塞 UI 线程. 可以考虑: Task.Run(Start)
. 或者在调用 GetContextAsync()
之后调用 ConfigureAwait(false)
.
需要注意的是, 虽然使用异步方法, 但是直到 await
才会处理异步.
使用 DNS
静态类 Dns
封装了 DNS 操作, 主要有两个方法:
GetHostAddress
, 将域名转换为 IP 地址.GetHostEntry
, 将 IP (字符串, 或IPAddress
) 转换为 域名.
在使用 WebRequest
或 TcpClient
等类时, 域名是自动转换为 IP 的. 如果一个处理中有大量请求, 可以提前将域名转换为 IP, 这样避免每次的转换, 性能会更好, 特别是处理传输层时 (例如: TcpClient
, UdpCLient
, 以及 Socket
).
Dns
类也提供了 异步版本的方法.
使用 SmtpClient 发送邮件
使用 System.Net.Mail
下的 SmtpClient
, 可以通过 SMTP, Simple Mail Transfer Protocol, 简单邮件传输协议来发送邮件. 要发送邮件, 只需要给 SmtpClient
实例的 Host
属性设置 SMTP
服务器的地址, 然后调用 Send
即可.
SmtpClient client = new SmtpClient();
client.Host = "mail.myserver.com";
client.Send("from@adomain.com", "to@adomain.com", "subject", "body");
更复杂的邮件信息可以通过构造 MailMessage
来实现, 包括附件.
...
MailMessage mm = new MailMessage();
mm.Sender = new MailAddress("kay@domain.com", "Kay");
mm.From = new MailAddress("kay@domain.com", "Kay");
mm.To.Add(new MailAddress("bob@domain.com", "Bob"));
mm.CC.Add(new MailAddress("dan@domain.com", Dan));
mm.Subject = "Hello!";
mm.Body = "Hi there. Here's the photo";
mm.IsBodyHtml = false;
mm.Priority = MailPriority.High;
Attachment a = new Attachment(
"photo.jpg",
System.Net.Mime.MediaTypeNames.Image.Jpeg
);
mm.Attachments.Add(a);
client.Send(mm);
大多数 SMTP 服务都在互联网环境下, 连接都需要鉴权:
var client = new SmtpClient("smtp.myisp.com", 587) {
Credentials = new NetworkCredential("username", "password"),
EnableSsl = true
};
...
通过修改 DeliveryMethod
属性, 可以设置使用 IIS 来发送邮件, 或将邮件内容写入 .eml
文件中. 这在开发中会很有用.
SmtpClient client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
client.PickupDirectoryLocation = @"c:\mail";