About this article
This article was created using an automated generation workflow leveraging generative AI. It verifies the HTTP/1.1 message structure of RFC 9112 and the .NET TcpClient / NetworkStream APIs, implementing them as a localhost experiment without connecting to external sites.Verification Status: 📘 RFC & official .NET APIs verified, localhost sample implemented, PowerShell live verification not yet performed
Sending Raw HTTP with PowerShell: Experiencing HTTP over TCP on Localhost
HTTP is not TCP itself. In HTTP/1.1, messages consisting of a request line, header fields, an empty line, and optionally a body are exchanged over a TCP connection.
Normally, Invoke-WebRequest or browsers hide these details, but TcpClient writing strings directly to the stream makes them easier to see.
Keeping it entirely on a single PC this time
Instead of using an external website as a testbed, we will open two PowerShell windows.
In one,
127.0.0.1:8085start a simple HTTP serverIn the other, establish a TCP connection
Send an HTTP/1.1 GET request
The server displays the received content
200 OKreturns
sequenceDiagram
participant C as PowerShell Client
participant T as TCP localhost:8085
participant S as PowerShell Server
C->>T: TCP connect
T->>S: connection accepted
C->>S: GET / HTTP/1.1 + headers + CRLF CRLF
S-->>C: HTTP/1.1 200 OK + headers + body
S-->>C: TCP close
The HTTP being sent looks like a plain string
The core of the client side is as follows.
$request = @(
"GET / HTTP/1.1"
"Host: localhost:8085"
"Connection: close"
""
""
) -join "`r`n"
$requestBytes = [System.Text.Encoding]::ASCII.GetBytes($request)
$stream.Write($requestBytes, 0, $requestBytes.Length)
$stream.Flush()
The HTTP/1.1 request line is GET / HTTP/1.1. This is followed by header fields, and the header section is terminated with an empty line.
Here, "" are placed side-by-side and joined with CRLF to create the final CRLF CRLF at the end.
Do not omit the Host header
RFC 9112 requires the Host header field in HTTP/1.1 requests. Just because this is educational material, sending only GET / HTTP/1.1 would hide an essential part of HTTP/1.1.
Therefore, even in this localhost experiment, we explicitly specify the following.
Host: localhost:8085
TcpClient does not know HTTP
.NET 's TcpClient is a class that handles TCP connections.GetStream() obtained via NetworkStream sends and receives byte arrays.
In other words, in this code,
flowchart TB
A[HTTPの文字列] --> B[ASCII bytes]
B --> C[NetworkStream]
C --> D[TCP]
D --> E[localhost server]
we are manually building the layer of
Once you grasp this concept, it becomes easier to understand that HTTP client APIs 'construct messages according to HTTP rules and use a transport layer underneath.'
The response Content-Length is the number of bytes, not characters
On the server side, the body is converted to UTF-8 before constructing the Content-Length.
$body = "Hello from localhost HTTP over TCP`n"
$bodyBytes = [System.Text.Encoding]::UTF8.GetBytes($body)
$header = @(
"HTTP/1.1 200 OK"
"Content-Type: text/plain; charset=utf-8"
"Content-Length: $($bodyBytes.Length)"
"Connection: close"
""
""
) -join "`r`n"
Content-Length represents the number of octets, so it is important not to directly use the character count of .Length when including multi-byte characters like Japanese in the body.
HTTPS is not exactly like this
This tutorial uses plain-text HTTP. In HTTPS, TLS is layered on top of TCP, and HTTP is exchanged over that.
HTTP TLS TCP IP
Therefore, simply sending an HTTP string to port 443 using this sample will not result in valid HTTPS communication. It can serve as an entry point when you learn TLS next.

