فصل ۱۶: HttpListener، DNS، SMTP، TCP و دریافت ایمیل POP3
فصل ۱۶: HttpListener، DNS، SMTP، TCP و دریافت ایمیل POP3
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
ادامهٔ Cookieها
Cookie c = new Cookie ("PREF",
"ID=6b10df1da493a9c4:TM=1179...",
"/",
".google.com");
freshCookieContainer.Add (c);
آرگومانهای سوم و چهارم بهترتیب Path و Domain مبدأ را مشخص میکنند. یک CookieContainer در سمت Client میتواند Cookieهایی از مکانهای متعدد نگه دارد؛ HttpClient فقط Cookieهایی را ارسال میکند که Path و Domain آنها با Server مقصد مطابقت داشته باشد.
نوشتن یک HTTP Server
var app = WebApplication.CreateBuilder().Build();
app.MapGet ("/", () => "Hello, world!");
app.Run();
با کلاس HttpListener نیز میتوانید HTTP Server خودتان را در .NET بنویسید. مثال زیر Server سادهای است که روی Port 51111 گوش میدهد، برای یک Request از Client منتظر میماند و سپس یک پاسخ یکخطی برمیگرداند:
using var server = new SimpleHttpServer();
// Make a client request:
Console.WriteLine (await new HttpClient().GetStringAsync
("http://localhost:51111/MyApp/Request.txt"));
class SimpleHttpServer : IDisposable
{
readonly HttpListener listener = new HttpListener();
public SimpleHttpServer() => ListenAsync();
async void ListenAsync()
{
listener.Prefixes.Add ("http://localhost:51111/MyApp/"); // Listen on
listener.Start(); // port 51111
// Await a client request:
HttpListenerContext context = await listener.GetContextAsync();
// Respond to the request:
string msg = "You asked for: " + context.Request.RawUrl;
context.Response.ContentLength64 = Encoding.UTF8.GetByteCount (msg);
context.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();
}
OUTPUT: You asked for: /MyApp/Request.txt
در Windows، HttpListener در داخل از اشیای Socket مربوط به .NET استفاده نمیکند؛ در عوض Windows HTTP Server API را فراخوانی میکند. به این ترتیب چندین Application روی یک کامپیوتر میتوانند روی همان IP Address و Port گوش دهند، به شرط آنکه هرکدام Address Prefix متفاوتی ثبت کنند. در مثال ما Prefix به شکل http://localhost/myapp ثبت شد، بنابراین Application دیگری آزاد است روی همان IP و Port اما Prefix دیگری مانند http://localhost/anotherapp گوش دهد. این موضوع ارزشمند است، زیرا باز کردن Portهای جدید روی Firewallهای سازمانی میتواند از نظر فرایندهای اداری دشوار باشد.
وقتی GetContext را فراخوانی میکنید، HttpListener برای Request بعدی Client منتظر میماند و شیئی با propertyهای Request و Response برمیگرداند. هرکدام از دید Server مشابه Request یا Response سمت Client هستند. برای مثال، تقریباً مانند سمت Client میتوانید Headerها و Cookieها را در اشیای Request و Response بخوانید و بنویسید.
بسته به Clientهایی که انتظار دارید با Server شما کار کنند، میتوانید تصمیم بگیرید چه مقدار از قابلیتهای HTTP Protocol را پشتیبانی کنید. در حداقل حالت، باید برای هر Request طول Content و Status Code را تنظیم کنید.
در ادامه یک Web Page Server بسیار ساده را میبینید که بهصورت asynchronous نوشته شده است:
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
class WebServer
{
HttpListener _listener;
string _baseFolder; // Your web page folder.
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; } // Listener stopped.
catch (InvalidOperationException) { break; } // Listener stopped.
}
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.Exists (path))
{
Console.WriteLine ("Resource not found: " + path);
context.Response.StatusCode = (int) HttpStatusCode.NotFound;
msg = Encoding.UTF8.GetBytes ("Sorry, that page does not exist");
}
else
{
context.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 ("Request error: " + ex); }
}
}
کد زیر Server را راه میاندازد:
// Listen on port 51111, serving files in d:\webroot:
var server = new WebServer ("http://localhost:51111/", @"d:\webroot");
try
{
server.Start();
Console.WriteLine ("Server running... press Enter to stop");
Console.ReadLine();
}
finally { server.Stop(); }
در سمت Client میتوانید این کد را با هر مرورگر وبی تست کنید؛ URI در این مورد http://localhost:51111/ بهاضافهٔ نام Web Page خواهد بود.
استفادهٔ ما از Functionهای asynchronous این Server را scalable و کارآمد میکند. بااینحال، اگر آن را از یک UI Thread اجرا کنیم، Scalability کاهش مییابد، زیرا پس از هر await، اجرای هر Request دوباره به UI Thread برمیگردد. تحمل چنین Overheadی بهخصوص وقتی Shared State نداریم بیفایده است؛ بنابراین در سناریوی UI بهتر است از UI Thread خارج شویم.
یک راه این است:
یا پس از فراخوانی GetContextAsync از ConfigureAwait(false) استفاده کنیم.
توجه کنید با وجود اینکه ProcessRequestAsync از قبل asynchronous است، ما برای فراخوانی آن از Task.Run استفاده کردیم. این کار به Caller اجازه میدهد بلافاصله Request بعدی را پردازش کند، بهجای اینکه ابتدا فاز synchronous متد، یعنی بخش تا اولین await، را منتظر بماند.
استفاده از DNS
کلاس static به نام Dns سیستم DNS را کپسوله میکند؛ سیستمی که بین یک IP Address خام مانند 66.135.192.87 و یک Domain Name قابل فهم برای انسان مانند ebay.com تبدیل انجام میدهد.
متد GetHostAddresses یک Domain Name را به یک یا چند IP Address تبدیل میکند:
foreach (IPAddress a in Dns.GetHostAddresses ("albahari.com"))
Console.WriteLine (a.ToString()); // 205.210.42.167
متد GetHostEntry مسیر برعکس را طی میکند و Address را به Domain Name تبدیل میکند:
IPHostEntry entry = Dns.GetHostEntry ("205.210.42.167");
Console.WriteLine (entry.HostName); // albahari.com
GetHostEntry یک شیء IPAddress نیز میپذیرد، بنابراین میتوانید IP Address را بهصورت Byte Array ارائه کنید:
IPAddress address = new IPAddress (new byte[] { 205, 210, 42, 167 });
IPHostEntry entry = Dns.GetHostEntry (address);
Console.WriteLine (entry.HostName); // albahari.com
هنگام استفاده از کلاسی مانند WebRequest یا TcpClient، Domain Nameها بهصورت خودکار به IP Address تبدیل میشوند. اما اگر در طول عمر Application قصد دارید تعداد زیادی Network Request به همان Address بفرستید، گاهی میتوانید Performance را با این روش بهتر کنید که ابتدا با Dns صریحاً Domain Name را به IP Address تبدیل کنید و سپس از آن به بعد مستقیماً با IP Address ارتباط بگیرید. این کار رفتوبرگشتهای تکراری برای Resolve کردن همان Domain Name را حذف میکند و هنگام کار در Transport Layer از طریق TcpClient، UdpClient یا Socket میتواند مفید باشد.
کلاس DNS متدهای asynchronous مبتنی بر Task و قابل await نیز دارد:
foreach (IPAddress a in await Dns.GetHostAddressesAsync ("albahari.com"))
Console.WriteLine (a.ToString());
ارسال ایمیل با SmtpClient
کلاس SmtpClient در فضای نام System.Net.Mail اجازه میدهد پیامهای ایمیل را از طریق پروتکل همهجا حاضر Simple Mail Transfer Protocol یا SMTP ارسال کنید.
برای ارسال یک پیام متنی ساده، SmtpClient را نمونهسازی کنید، property آن به نام Host را برابر Address مربوط به SMTP Server قرار دهید و سپس Send را فراخوانی کنید:
SmtpClient client = new SmtpClient();
client.Host = "mail.myserver.com";
client.Send ("from@adomain.com", "to@adomain.com", "subject", "body");
ساختن یک MailMessage گزینههای بیشتری در اختیار میگذارد، از جمله امکان افزودن Attachment:
SmtpClient client = new SmtpClient();
client.Host = "mail.myisp.net";
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);
برای مقابله با Spammerها، بیشتر SMTP Serverهای روی Internet فقط Connectionهای احراز هویتشده را میپذیرند و ارتباط روی SSL را الزامی میکنند:
var client = new SmtpClient ("smtp.myisp.com", 587)
{
Credentials = new NetworkCredential ("me@myisp.com", "MySecurePass"),
EnableSsl = true
};
client.Send ("me@myisp.com", "someone@somewhere.com", "Subject", "Body");
Console.WriteLine ("Sent");
با تغییر property به نام DeliveryMethod میتوانید به SmtpClient بگویید بهجای روش عادی از IIS برای ارسال Mail Message استفاده کند، یا هر Message را بهسادگی در یک فایل با پسوند .eml در Directory مشخص بنویسد. این قابلیت هنگام Development مفید است:
SmtpClient client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
client.PickupDirectoryLocation = @"c:\mail";
استفاده از TCP
TCP و UDP پروتکلهای Transport Layer هستند که بیشتر سرویسهای Internet و LAN روی آنها ساخته میشوند. HTTP نسخهٔ 2 و پایینتر، FTP و SMTP از TCP استفاده میکنند؛ DNS و HTTP نسخهٔ 3 از UDP استفاده میکنند. TCP اتصالگرا (connection-oriented) است و سازوکارهای Reliability دارد؛ UDP بدون Connection است، Overhead کمتری دارد و از Broadcasting پشتیبانی میکند. BitTorrent از UDP استفاده میکند و Voice over IP یا VoIP نیز همینطور.
Transport Layer نسبت به لایههای بالاتر انعطافپذیری بیشتر و بهطور بالقوه Performance بهتری ارائه میدهد، اما شما را ملزم میکند کارهایی مانند Authentication و Encryption را خودتان مدیریت کنید.
برای TCP در .NET میتوانید از کلاسهای Facade سادهتر TcpClient و TcpListener یا کلاس قدرتمند و پرقابلیت Socket استفاده کنید. در واقع میتوانید آنها را با هم ترکیب کنید، زیرا TcpClient از طریق property به نام Client شیء Socket زیرین را در اختیار میگذارد. Socket گزینههای Configuration بیشتری ارائه میکند و دسترسی مستقیم به Network Layer یعنی IP و پروتکلهای غیراینترنتی مانند SPX/IPX شرکت Novell را نیز ممکن میسازد.
همانند دیگر پروتکلها، TCP میان Client و Server تفاوت قائل میشود: Client Request را آغاز میکند و Server منتظر Request میماند. ساختار پایهٔ یک TCP Client Request بهصورت synchronous چنین است:
using (TcpClient client = new TcpClient())
{
client.Connect ("address", port);
using (NetworkStream n = client.GetStream())
{
// Read and write to the network stream...
}
}
متد Connect در TcpClient تا برقراری Connection بلاک میشود؛ ConnectAsync معادل asynchronous آن است. سپس NetworkStream وسیلهای برای ارتباط دوطرفه فراهم میکند تا Byteها را هم به Server ارسال و هم از آن دریافت کنید.
یک TCP Server ساده به این شکل است:
TcpListener listener = new TcpListener (<ip address>, port);
listener.Start();
while (keepProcessingRequests)
using (TcpClient c = listener.AcceptTcpClient())
using (NetworkStream n = c.GetStream())
{
// Read and write to the network stream...
}
listener.Stop();
TcpListener به Local IP Addressی نیاز دارد که باید روی آن Listen کند؛ برای مثال کامپیوتری با دو Network Card ممکن است دو Address داشته باشد. میتوانید از IPAddress.Any استفاده کنید تا روی همهٔ Local IP Addressها یا تنها Address موجود Listen کند. AcceptTcpClient تا زمان دریافت Request از Client بلاک میشود؛ اینجا هم نسخهٔ asynchronous وجود دارد. سپس مانند سمت Client، GetStream را فراخوانی میکنیم.
هنگام کار در Transport Layer باید پروتکلی تعیین کنید که مشخص کند چه کسی، چه زمانی و برای چه مدت صحبت میکند؛ چیزی شبیه Walkie-Talkie. اگر هر دو طرف همزمان صحبت کنند یا همزمان گوش بدهند، ارتباط از کار میافتد.
فرض کنیم پروتکلی میسازیم که Client ابتدا عبارت «Hello» را میگوید و Server در پاسخ «Hello right back!» میفرستد. کد:
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
new Thread (Server).Start(); // Run server method concurrently.
Thread.Sleep (500); // Give server time to start.
Client();
void Client()
{
using (TcpClient client = new TcpClient ("localhost", 51111))
using (NetworkStream n = client.GetStream())
{
BinaryWriter w = new BinaryWriter (n);
w.Write ("Hello");
w.Flush();
Console.WriteLine (new BinaryReader (n).ReadString());
}
}
void Server() // Handles a single client request, then exits.
{
TcpListener listener = new TcpListener (IPAddress.Any, 51111);
listener.Start();
using (TcpClient c = listener.AcceptTcpClient())
using (NetworkStream n = c.GetStream())
{
string msg = new BinaryReader (n).ReadString();
BinaryWriter w = new BinaryWriter (n);
w.Write (msg + " right back!");
w.Flush(); // Must call Flush because we're not
} // disposing the writer.
listener.Stop();
}
// OUTPUT: Hello right back!
در این مثال از Loopback به نام localhost استفاده کردهایم تا Client و Server را روی یک Machine اجرا کنیم. بهصورت دلخواه Portی در محدودهٔ تخصیصنیافته، بالاتر از 49152، انتخاب کرده و برای Encode کردن Messageهای متنی از BinaryWriter و BinaryReader استفاده کردهایم. Readerها و Writerها را Close یا Dispose نکردهایم تا NetworkStream زیرین تا پایان Conversation باز بماند.
BinaryReader و BinaryWriter ممکن است برای خواندن و نوشتن String انتخاب عجیبی به نظر برسند، اما نسبت به StreamReader و StreamWriter مزیت بزرگی دارند: قبل از String یک Integer قرار میدهند که Length را نشان میدهد، بنابراین BinaryReader همیشه دقیقاً میداند چند Byte باید بخواند. اگر StreamReader.ReadToEnd را فراخوانی کنید، ممکن است برای همیشه بلاک شوید، چون NetworkStream انتهای مشخصی ندارد.
تا زمانی که Connection باز است، Network Stream هیچگاه نمیتواند مطمئن باشد Client قرار نیست Data بیشتری بفرستد.
Concurrency با TCP
TcpClient و TcpListener برای Concurrency مقیاسپذیر، متدهای asynchronous مبتنی بر Task ارائه میکنند. استفاده از آنها صرفاً یعنی متدهای Blocking را با نسخههای *Async جایگزین کنید و Task برگشتی را await کنید.
در مثال زیر یک TCP Server asynchronous مینویسیم که Requestهایی با طول 5,000 Byte میپذیرد، ترتیب Byteها را معکوس میکند و آنها را دوباره برای Client میفرستد:
async void RunServerAsync ()
{
var listener = new TcpListener (IPAddress.Any, 51111);
listener.Start ();
try
{
while (true)
Accept (await listener.AcceptTcpClientAsync ());
}
finally { listener.Stop(); }
}
async Task Accept (TcpClient client)
{
await Task.Yield ();
try
{
using (client)
using (NetworkStream n = client.GetStream ())
{
byte[] data = new byte [5000];
int bytesRead = 0; int chunkSize = 1;
while (bytesRead < data.Length && chunkSize > 0)
bytesRead += chunkSize =
await n.ReadAsync (data, bytesRead, data.Length - bytesRead);
Array.Reverse (data); // Reverse the byte sequence
await n.WriteAsync (data, 0, data.Length);
}
}
catch (Exception ex) { Console.WriteLine (ex.Message); }
}
چنین برنامهای scalable است، زیرا در تمام مدت Request یک Thread را بلاک نمیکند. بنابراین اگر 1,000 Client همزمان روی یک Connection شبکهٔ کند متصل شوند و هر Request چند ثانیه طول بکشد، برخلاف راهحل synchronous، این برنامه در تمام آن مدت به 1,000 Thread نیاز ندارد. در عوض، Threadها را فقط برای بازههای کوتاهی اجاره میکند که برای اجرای Code قبل و بعد از expressionهای await لازم است.
دریافت ایمیل POP3 با TCP
.NET در Application Layer برای POP3 پشتیبانی آمادهای ندارد؛ بنابراین برای دریافت Mail از POP3 Server باید در سطح TCP برنامهنویسی کنید. خوشبختانه این Protocol ساده است. یک Conversation در POP3 چنین پیش میرود:
گفتوگوی نمونهٔ POP3| Client | Mail Server | توضیح |
|---|
| Client connects... | +OK Hello there. | Welcome Message |
| USER joe | +OK Password required. | |
| PASS password | +OK Logged in. | |
| LIST | +OK 1 1876 2 5412 3 845 . | ID و File Size هر Message روی Server را فهرست میکند. |
| RETR 1 | +OK 1876 octets Content of message #1... . | Message با ID مشخص را دریافت میکند. |
| DELE 1 | +OK Deleted. | Message را از Server حذف میکند. |
| QUIT | +OK Bye-bye. | |
هر Command و Response با یک New Line، یعنی CR + LF، خاتمه مییابد؛ بهجز Commandهای چندخطی LIST و RETR که با یک Dot منفرد در یک Line جدا تمام میشوند. چون نمیتوانیم StreamReader را با NetworkStream استفاده کنیم، ابتدا یک Helper Method مینویسیم که یک خط Text را بدون Buffer بخواند:
string ReadLine (Stream s)
{
List<byte> lineBuffer = new List<byte>();
while (true)
{
int b = s.ReadByte();
if (b == 10 || b < 0) break;
if (b != 13) lineBuffer.Add ((byte)b);
}
return Encoding.UTF8.GetString (lineBuffer.ToArray());
}
به یک Helper Method برای ارسال Command هم نیاز داریم. چون همیشه انتظار Responseای را داریم که با +OK آغاز شود، میتوانیم همانزمان Response را بخوانیم و Validate کنیم:
void SendCommand (Stream stream, string line)
{
byte[] data = Encoding.UTF8.GetBytes (line + "\r\n");
stream.Write (data, 0, data.Length);
string response = ReadLine (stream);
if (!response.StartsWith ("+OK"))
throw new Exception ("POP Error: " + response);
}
با نوشتهشدن این Methodها، دریافت Mail آسان است. روی Port 110، یعنی Port پیشفرض POP3، یک TCP Connection برقرار میکنیم و سپس با Server صحبت میکنیم. در این مثال، پیش از حذف Message از Server، هر Mail Message را در یک File با نام تصادفی و پسوند .eml مینویسیم:
using (TcpClient client = new TcpClient ("mail.isp.com", 110))
using (NetworkStream n = client.GetStream())
{
ReadLine (n); // Read the welcome message.
SendCommand (n, "USER username");
SendCommand (n, "PASS password");
SendCommand (n, "LIST"); // Retrieve message IDs
List<int> messageIDs = new List<int>();
while (true)
{
string line = ReadLine (n); // e.g., "1 1876"
if (line == ".") break;
messageIDs.Add (int.Parse (line.Split (' ')[0] )); // Message ID
}
foreach (int id in messageIDs) // Retrieve each message.
{
SendCommand (n, "RETR " + id);
string randomFile = Guid.NewGuid().ToString() + ".eml";
using (StreamWriter writer = File.CreateText (randomFile))
while (true)
{
string line = ReadLine (n); // Read next line of message.
if (line == ".") break; // Single dot = end of message.
if (line == "..") line = "."; // "Escape out" double dot.
writer.WriteLine (line); // Write to output file.
}
SendCommand (n, "DELE " + id); // Delete message off server.
}
SendCommand (n, "QUIT");
}