本文属于基础入门博客,将和大家介绍如何在 dotnet C# 代码里面使用底层的 Socket 进行 HTTP 网络请求

本文将使用向百度发送 HTTP 和 HTTPS 请求作为示例,来和大家样式如何使用底层的 Socket 进行 HTTP 和 HTTPS 网络请求

本文开始之前,希望大家对基础的网络知识有所了解

回顾进行 HTTP 请求的基本流程。拿到 https://www.baidu.com 之后,需要先经过 DNS 解析,获取到对应的 IP 地址。接着通过 TCP 协议和服务器建立连接,发送 HTTP 请求内容,等待服务器响应内容

尽管通过 Socket 也能通过 DnsEndPoint 进行网络请求,但是为了尽可能展示更多的细节,本文将调用 Dns.GetHostAddressesAsync 方法对域名进行解析,获取到对应的 IP 地址。接着通过 Socket 类和服务器建立连接,发送 HTTP 请求内容,等待服务器响应内容

调用 Dns.GetHostAddressesAsync 对给定的域名进行解析的代码如下。本文内容里面只给出关键代码片段,如需要全部的项目文件,可到本文末尾找到本文所有代码的下载方法

// 一个域名可以有多个 IP 地址,利用此特性,可以实现 IP 级域名备份,也能利用此特性实现寻找距离自己最近的 IP 地址
IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync("www.baidu.com");

在我当前的网络环境下,能够获取到两个百度的地址。一般而言,取其首个满足条件的 IP 地址即可。有些时候需要禁用 Ip v6 地址的情况,则请大家自行判断和处理,本文不再赘述

额外说明一点,从 Dns.GetHostAddressesAsync 里面获取到的 IP 地址是不保证顺序的。从 DNS 提供商的角度上讲,本身也是不保证顺序的,大家可以试试去找一些 CDN 厂商的域名试试看。为什么说去找 CDN 厂商的域名呢?因为 CDN 厂商的域名一般都是有多个 IP 地址的。大家可以试试在 CMD 命令行里面不断输入 ipconfig /flushdns 命令来清除 DNS 缓存,然后再调用 Dns.GetHostAddressesAsync 获取 IP 地址,测试每次获取到的地址是否顺序相同

为了演示方便,我将遍历百度的所有 IP 地址,依次进行 HTTP 请求。进行 HTTP 请求时,获取到了 IP 地址,就可以开始使用 Socket 类进行连接了。在连接之前,先从共享池中租用一个字节数组,为 1MB 大小。网络请求过程中也是可以做到低分配的,合理使用内存池可以减少 GC 压力。代码如下

var buffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);

try
{
    foreach (var ipAddress in ipAddresses)
    {
        ... // 忽略其他代码
    }
}
finally
{
    // 别忘了归还共享池中的字节数组
    ArrayPool<byte>.Shared.Return(buffer);
}

开始创建 Socket 对象,创建完成之后,调用 ConnectAsync 方法进行连接。默认情况下的 HTTP 应该连接的是 80 端口

    foreach (var ipAddress in ipAddresses)
    {
        Console.WriteLine($"开始连接 IP:{ipAddress}");

        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ipAddress, 80);

        ... // 忽略其他代码
    }

预期能够连接成功,此时就完成了 HTTP 连接中的 TCP 建立过程了。下一步是发送 HTTP 请求内容。这里的请求内容是一个简单的 HTTP GET 请求。在 dotnet 里面封装了 NetworkStream 类,可以简化一些网络通讯代码。可以像一个 Stream 一样进行网络通讯,整个代码写起来还是比较舒服的。咱就在此基础上构建一个 NetworkStream 对象,然后通过 Stream 的方式写入请求内容,注:后面要带两个换行哦。代码如下

        using var networkStream = new NetworkStream(socket);

        Console.WriteLine($"连接完成,开始发送请求");

        // 这里的请求内容是一个简单的 HTTP GET 请求,注:后面要带两个换行哦
        ReadOnlySpan<byte> content = """
                                     GET https://www.baidu.com HTTP/1.1
                                     Host: www.baidu.com


                                     """u8;
        content.CopyTo(buffer); // 将请求内容复制到租用的字节数组中。异步请求不能传入 Span 类型,只能传入 Memory 类型。将 Span 转换为 Memory 的方式是先写入到 buffer 中,然后再将其当成 ReadOnlyMemory 或 Memory 类型
        ReadOnlyMemory<byte> writeBuffer = buffer.AsMemory(0, content.Length);
        await networkStream.WriteAsync(writeBuffer);

写入之后,依然可以通过 NetworkStream 读取百度服务器响应的内容,代码如下

        // 读取响应内容
        var length = await networkStream.ReadAsync(buffer); // 注: 很多时候,这里都是没有完全读取到完整的响应内容的,可能需要多次读取才能获取完整的响应内容。读取多长的数据需要从返回的 Header 里面获取 Content-Length 字段的值
        var text = Encoding.UTF8.GetString(buffer, 0, length);

        Console.WriteLine($"收到百度的响应内容。内容长度 {length},内容摘要:{text.Substring(0, Math.Min(50, text.Length))}...");

        Console.WriteLine();

以上的代码没有去读取 Content-Length 字段的值,也没有处理一次性读取没有读取完的问题。如果大家想要制作一个合理的 HTTP 请求客户端,则需要自行处理这些问题。本文只是一个简单的示例,演示如何使用底层的 Socket 进行 HTTP 请求,没有处理这些细节

以上代码就完成了 HTTP 的请求了。那么如何进行 HTTPS 的请求呢?其实和 HTTP 的请求是类似的,只不过需要在 NetworkStream 的基础上,使用 SslStream 进行封装。依然是和 HTTP 一样的前置过程,获取 IP 地址,创建 Socket 对象,连接到服务器。相同部分的代码如下,当然别忘了改一下端口号哦

        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ipAddress, 443); // 443 是 HTTPS 的默认端口

        using var networkStream = new NetworkStream(socket);

完成连接之后,接下来就是其不同的部分。不同的是,连接到服务器之后,需要进行 SSL/TLS 握手,使用 SslStream 包装 NetworkStream,然后进行认证。接下来后续的所有读写操作都将通过 SslStream 进行。代码如下

        // 进行 SSL/TLS 握手,使用 SslStream 包装 NetworkStream 然后进行认证
        // 接下来后续的所有读写操作都将通过 SslStream 进行
        using var sslStream = new SslStream(networkStream);
        await sslStream.AuthenticateAsClientAsync("www.baidu.com");

在 dotnet 里面,无论是 NetworkStream 还是 SslStream 都是继承自 Stream 类的,所以可以使用相同的方式进行读写操作。接下来就是发送请求内容了,和 HTTP 的请求内容一样,只不过需要使用 SslStream 来发送请求内容。代码如下

        // 这里的请求内容是一个简单的 HTTP GET 请求,注:后面要带两个换行哦
        ReadOnlySpan<byte> content = """
                                     GET https://www.baidu.com HTTP/1.1
                                     Host: www.baidu.com


                                     """u8;
        content.CopyTo(buffer); // 将请求内容复制到租用的字节数组中。异步请求不能传入 Span 类型,只能传入 Memory 类型。将 Span 转换为 Memory 的方式是先写入到 buffer 中,然后再将其当成 ReadOnlyMemory 或 Memory 类型
        ReadOnlyMemory<byte> writeBuffer = buffer.AsMemory(0, content.Length);
        await sslStream.WriteAsync(writeBuffer); // 这里要用 SslStream 来发送请求内容

可以对比一下 HTTP 请求的代码,只会发现 WriteAsync 的对象从 NetworkStream 类型换成 SslStream 类型而已

读取百度响应的代码也是类似,代码如下

        // 读取响应内容
        var length = await sslStream.ReadAsync(buffer); // 这里要用 SslStream 来读取响应内容
        var text = Encoding.UTF8.GetString(buffer, 0, length);

        Console.WriteLine($"收到百度的响应内容。内容长度 {length},内容摘要:{text.Substring(0, Math.Min(50, text.Length))}...");

至少在 dotnet 的封装基础之下,即使使用十分底层的 Socket 方式进行 HTTP 或 HTTPS 通讯,其代码也是十分简洁的。以上就是本文的全部演示内容了,网络通讯是一个知识量比较庞大的领域,本文只是一个简单的示例,演示如何使用底层的 Socket 进行 HTTP 和 HTTPS 网络请求。希望能够对大家有所帮助

全部的 Program.cs 文件的代码如下

// See https://aka.ms/new-console-template for more information

using System.Buffers;
using System.Net;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;

// 一个域名可以有多个 IP 地址,利用此特性,可以实现 IP 级域名备份,也能利用此特性实现寻找距离自己最近的 IP 地址
IPAddress[] ipAddresses = await Dns.GetHostAddressesAsync("www.baidu.com");

// 从共享池中租用一个字节数组,大小为 1MB
// 网络请求过程中也是可以做到低分配的,合理使用内存池可以减少 GC 压力
var buffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);

try
{
    foreach (var ipAddress in ipAddresses)
    {
        Console.WriteLine($"开始连接 IP:{ipAddress}");

        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ipAddress, 80);

        using var networkStream = new NetworkStream(socket);

        Console.WriteLine($"连接完成,开始发送请求");

        // 这里的请求内容是一个简单的 HTTP GET 请求,注:后面要带两个换行哦
        ReadOnlySpan<byte> content = """
                                     GET https://www.baidu.com HTTP/1.1
                                     Host: www.baidu.com


                                     """u8;
        content.CopyTo(buffer); // 将请求内容复制到租用的字节数组中。异步请求不能传入 Span 类型,只能传入 Memory 类型。将 Span 转换为 Memory 的方式是先写入到 buffer 中,然后再将其当成 ReadOnlyMemory 或 Memory 类型
        ReadOnlyMemory<byte> writeBuffer = buffer.AsMemory(0, content.Length);
        await networkStream.WriteAsync(writeBuffer);

        // 读取响应内容
        var length = await networkStream.ReadAsync(buffer); // 注: 很多时候,这里都是没有完全读取到完整的响应内容的,可能需要多次读取才能获取完整的响应内容。读取多长的数据需要从返回的 Header 里面获取 Content-Length 字段的值
        var text = Encoding.UTF8.GetString(buffer, 0, length);

        Console.WriteLine($"收到百度的响应内容。内容长度 {length},内容摘要:{text.Substring(0, Math.Min(50, text.Length))}...");

        Console.WriteLine();
    }
}
finally
{
    // 别忘了归还共享池中的字节数组
    ArrayPool<byte>.Shared.Return(buffer);
}

// 以下是进行 https 请求的代码
// 上面的缓存已经被归还了,为了继续使用,就开始重新申请好了
buffer = ArrayPool<byte>.Shared.Rent(1024 * 1024);

try
{
    foreach (var ipAddress in ipAddresses)
    {
        Console.WriteLine($"开始连接 IP:{ipAddress}");

        using var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ipAddress, 443); // 443 是 HTTPS 的默认端口

        using var networkStream = new NetworkStream(socket);

        Console.WriteLine($"连接完成,开始进行 https 通讯");

        // 进行 SSL/TLS 握手,使用 SslStream 包装 NetworkStream 然后进行认证
        // 接下来后续的所有读写操作都将通过 SslStream 进行
        using var sslStream = new SslStream(networkStream);
        await sslStream.AuthenticateAsClientAsync("www.baidu.com");

        Console.WriteLine($"开始发送请求");

        // 这里的请求内容是一个简单的 HTTP GET 请求,注:后面要带两个换行哦
        ReadOnlySpan<byte> content = """
                                     GET https://www.baidu.com HTTP/1.1
                                     Host: www.baidu.com


                                     """u8;
        content.CopyTo(buffer); // 将请求内容复制到租用的字节数组中。异步请求不能传入 Span 类型,只能传入 Memory 类型。将 Span 转换为 Memory 的方式是先写入到 buffer 中,然后再将其当成 ReadOnlyMemory 或 Memory 类型
        ReadOnlyMemory<byte> writeBuffer = buffer.AsMemory(0, content.Length);
        await sslStream.WriteAsync(writeBuffer); // 这里要用 SslStream 来发送请求内容

        // 读取响应内容
        var length = await sslStream.ReadAsync(buffer); // 这里要用 SslStream 来读取响应内容
        var text = Encoding.UTF8.GetString(buffer, 0, length);

        Console.WriteLine($"收到百度的响应内容。内容长度 {length},内容摘要:{text.Substring(0, Math.Min(50, text.Length))}...");

        Console.WriteLine();
    }
}
finally
{
    // 别忘了归还共享池中的字节数组
    ArrayPool<byte>.Shared.Return(buffer);
}

本文代码放在 githubgitee 上,可以使用如下命令行拉取代码。我整个代码仓库比较庞大,使用以下命令行可以进行部分拉取,拉取速度比较快

先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin cb55daad2c8e5725e7b780261939bf5728e5323a

以上使用的是国内的 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码。如果依然拉取不到代码,可以发邮件向我要代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin cb55daad2c8e5725e7b780261939bf5728e5323a

获取代码之后,进入 Workbench/HalaiheakerbarheeLayneberqarke 文件夹,即可获取到源代码

更多技术博客,请参阅 博客导航


本文会经常更新,请阅读原文: https://blog.lindexi.com/post/dotnet-C-%E5%85%A5%E9%97%A8%E7%A4%BA%E4%BE%8B-%E7%94%A8%E5%BA%95%E5%B1%82%E7%9A%84-Socket-%E8%BF%9B%E8%A1%8C-HTTP-%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

如果你想持续阅读我的最新博客,请点击 RSS 订阅,推荐使用RSS Stalker订阅博客,或者收藏我的博客导航

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系

微软最具价值专家


无盈利,不卖课,做纯粹的技术博客

以下是广告时间

推荐关注 Edi.Wang 的公众号

欢迎进入 Eleven 老师组建的 .NET 社区

以上广告全是友情推广,无盈利