first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
k8s/
|
||||||
|
.git/
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Normalize line endings: store everything as LF in the repository.
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Windows-only scripts keep CRLF on checkout
|
||||||
|
*.ps1 text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
|
||||||
|
# Binary files
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.ico binary
|
||||||
|
*.dll binary
|
||||||
|
*.exe binary
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.user
|
||||||
|
.vs/
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||||
|
WORKDIR /src
|
||||||
|
COPY src/TcpEcho/TcpEcho.csproj TcpEcho/
|
||||||
|
RUN dotnet restore TcpEcho/TcpEcho.csproj
|
||||||
|
COPY src/TcpEcho/ TcpEcho/
|
||||||
|
RUN dotnet publish TcpEcho/TcpEcho.csproj -c Release -o /app --no-restore
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/runtime:8.0-alpine
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG REVISION=unknown
|
||||||
|
ARG CREATED=""
|
||||||
|
# OCI labels: Gitea's package registry displays these on the package page and, when
|
||||||
|
# image.source matches a repository URL, links the package to that repository.
|
||||||
|
LABEL org.opencontainers.image.title="tcp-echo-server" \
|
||||||
|
org.opencontainers.image.description="Minimal .NET 8 TCP echo server that prefixes every reply with its instance identifier. Deployed as two instances behind Traefik on ports 9098 and 9099." \
|
||||||
|
org.opencontainers.image.source="https://gitea.eitczech.com/public/tcp-echo-server" \
|
||||||
|
org.opencontainers.image.url="https://gitea.eitczech.com/public/tcp-echo-server" \
|
||||||
|
org.opencontainers.image.documentation="https://gitea.eitczech.com/public/tcp-echo-server/src/branch/main/README.md" \
|
||||||
|
org.opencontainers.image.vendor="eitczech" \
|
||||||
|
org.opencontainers.image.licenses="MIT" \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.created="${CREATED}"
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=build /app .
|
||||||
|
ENV PORT=9099
|
||||||
|
EXPOSE 9099
|
||||||
|
USER 1654
|
||||||
|
ENTRYPOINT ["dotnet", "TcpEcho.dll"]
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# tcp-echo
|
||||||
|
|
||||||
|
Minimal .NET 8 TCP echo server. Every reply is prefixed with the instance identifier
|
||||||
|
(`INSTANCE_ID` env var, falling back to the hostname), and a `[<id>] connected` line is sent on connect.
|
||||||
|
|
||||||
|
Two instances are deployed behind Traefik:
|
||||||
|
|
||||||
|
| Instance | INSTANCE_ID | External port |
|
||||||
|
|----------|-------------|---------------|
|
||||||
|
| a | echo-a | 9098 |
|
||||||
|
| b | echo-b | 9099 |
|
||||||
|
|
||||||
|
## Build and push
|
||||||
|
|
||||||
|
Requires a prior `docker login gitea.eitczech.com`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
./build-push.ps1 -Tag 1.0.0 # pushes gitea.eitczech.com/public/tcp-echo-server:1.0.0
|
||||||
|
./build-push.ps1 -Tag 1.0.0 -AlsoLatest # additionally pushes :latest
|
||||||
|
```
|
||||||
|
|
||||||
|
The manifests reference `gitea.eitczech.com/public/tcp-echo-server:latest`. To pin a specific tag either edit
|
||||||
|
`k8s/instance-a.yaml` / `k8s/instance-b.yaml`, or after `kubectl apply -k` run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl -n tcp-echo set image deploy/tcp-echo-a deploy/tcp-echo-b tcp-echo=gitea.eitczech.com/public/tcp-echo-server:1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
If the Gitea registry is private, create a pull secret in the namespace and reference it via `imagePullSecrets`.
|
||||||
|
|
||||||
|
### Package description on Gitea
|
||||||
|
|
||||||
|
The `Dockerfile` sets the standard OCI labels (`org.opencontainers.image.*`). Gitea reads them from the pushed
|
||||||
|
image and shows them on the package page:
|
||||||
|
|
||||||
|
| Label | Shown as / effect |
|
||||||
|
|------------------|--------------------------------------------------------------------------------|
|
||||||
|
| `description` | Package description text |
|
||||||
|
| `source` | Links the package to the repository with the same URL (repo must exist first) |
|
||||||
|
| `url`, `documentation`, `licenses`, `authors`, `vendor` | Metadata section on the package page |
|
||||||
|
| `version`, `revision`, `created` | Filled in by `build-push.ps1` from the tag, git commit and current time |
|
||||||
|
|
||||||
|
To change the description, edit the `LABEL` in `Dockerfile` and rebuild. If the package was pushed before the
|
||||||
|
repository existed, link it manually: package page -> **Settings** -> **Link to repository**.
|
||||||
|
|
||||||
|
## Deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
kubectl apply -k k8s/
|
||||||
|
kubectl -n tcp-echo rollout status deploy/tcp-echo-a deploy/tcp-echo-b
|
||||||
|
```
|
||||||
|
|
||||||
|
`30-traefik-entrypoint.yaml` adds `tcpechoa` (9098) and `tcpechob` (9099) entrypoints to the k3s-managed
|
||||||
|
Traefik (Traefik restarts once). For a Helm-managed Traefik, drop that file from `kustomization.yaml` and run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
helm upgrade traefik traefik/traefik -n kube-system --reuse-values -f k8s/traefik-values.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
If Traefik's Service is a LoadBalancer, make sure the cloud/VPS firewall allows TCP 9098 and 9099.
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
### Linux / macOS
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ nc <server-ip> 9098
|
||||||
|
[echo-a] connected
|
||||||
|
hello
|
||||||
|
[echo-a] hello
|
||||||
|
|
||||||
|
$ nc <server-ip> 9099
|
||||||
|
[echo-b] connected
|
||||||
|
hello
|
||||||
|
[echo-b] hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (PowerShell)
|
||||||
|
|
||||||
|
Windows has no `nc`. Use the included script, which opens a TCP connection, prints the greeting,
|
||||||
|
sends a message and prints the echoed reply:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
. est-echo.ps1 -Server <server-ip> -Port 9098
|
||||||
|
. est-echo.ps1 -Server <server-ip> -Port 9099 -Message "ping"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```
|
||||||
|
Connecting to <server-ip>:9098 ...
|
||||||
|
<- [echo-a] connected
|
||||||
|
-> hello
|
||||||
|
<- [echo-a] hello
|
||||||
|
```
|
||||||
|
|
||||||
|
Just to check the port is reachable (no echo):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Test-NetConnection -ComputerName <server-ip> -Port 9098
|
||||||
|
```
|
||||||
|
|
||||||
|
For an interactive session, `telnet <server-ip> 9098` works if the Telnet Client feature is enabled
|
||||||
|
(`Enable-WindowsOptionalFeature -Online -FeatureName TelnetClient`), or install `ncat` via `winget install Insecure.Nmap`.
|
||||||
|
|
||||||
|
### Local test with Docker
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker build -t tcp-echo:local .
|
||||||
|
docker run --rm -d --name tcp-echo-test -e INSTANCE_ID=echo-local -p 9099:9099 tcp-echo:local
|
||||||
|
. est-echo.ps1 -Server localhost -Port 9099
|
||||||
|
docker rm -f tcp-echo-test
|
||||||
|
```
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Builds the tcp-echo Docker image and pushes it to the Gitea registry.
|
||||||
|
.EXAMPLE
|
||||||
|
.\build-push.ps1 -Tag 1.0.0
|
||||||
|
.\build-push.ps1 -Tag 1.0.0 -AlsoLatest
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true, Position = 0)]
|
||||||
|
[ValidatePattern('^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$')]
|
||||||
|
[string]$Tag,
|
||||||
|
|
||||||
|
# Also tag and push the image as :latest
|
||||||
|
[switch]$AlsoLatest
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$Registry = 'gitea.eitczech.com'
|
||||||
|
$Repo = "$Registry/public/tcp-echo-server"
|
||||||
|
$Image = "${Repo}:$Tag"
|
||||||
|
$Root = $PSScriptRoot
|
||||||
|
|
||||||
|
function Invoke-Checked {
|
||||||
|
param([string[]]$Cmd)
|
||||||
|
Write-Host "> $($Cmd -join ' ')" -ForegroundColor Cyan
|
||||||
|
& $Cmd[0] $Cmd[1..($Cmd.Length - 1)]
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Command failed with exit code ${LASTEXITCODE}: $($Cmd -join ' ')" }
|
||||||
|
}
|
||||||
|
|
||||||
|
# Metadata baked into OCI labels (shown on the Gitea package page)
|
||||||
|
$Revision = 'unknown'
|
||||||
|
if (Get-Command git -ErrorAction SilentlyContinue) {
|
||||||
|
$r = (& git -C $Root rev-parse --short HEAD 2>$null)
|
||||||
|
if ($LASTEXITCODE -eq 0 -and $r) { $Revision = $r.Trim() }
|
||||||
|
}
|
||||||
|
$Created = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
|
||||||
|
|
||||||
|
Write-Host "Building $Image (revision $Revision)" -ForegroundColor Green
|
||||||
|
Invoke-Checked @('docker', 'build', '--pull',
|
||||||
|
'--build-arg', "VERSION=$Tag",
|
||||||
|
'--build-arg', "REVISION=$Revision",
|
||||||
|
'--build-arg', "CREATED=$Created",
|
||||||
|
'-t', $Image, $Root)
|
||||||
|
|
||||||
|
if ($AlsoLatest) {
|
||||||
|
Invoke-Checked @('docker', 'tag', $Image, "${Repo}:latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Pushing $Image" -ForegroundColor Green
|
||||||
|
Invoke-Checked @('docker', 'push', $Image)
|
||||||
|
|
||||||
|
if ($AlsoLatest) {
|
||||||
|
Invoke-Checked @('docker', 'push', "${Repo}:latest")
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Done: $Image" -ForegroundColor Green
|
||||||
|
Write-Host "Deploy with: kubectl -n tcp-echo set image deploy/tcp-echo-a deploy/tcp-echo-b tcp-echo=$Image"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Traefik must have static entrypoints on 9098/9099 before IngressRouteTCP can use them.
|
||||||
|
# This is the k3s way (Traefik is installed via the bundled Helm chart). Traefik restarts once when applied.
|
||||||
|
# If Traefik was installed with plain Helm instead, use k8s/traefik-values.yaml with `helm upgrade`.
|
||||||
|
apiVersion: helm.cattle.io/v1
|
||||||
|
kind: HelmChartConfig
|
||||||
|
metadata:
|
||||||
|
name: traefik
|
||||||
|
namespace: kube-system
|
||||||
|
spec:
|
||||||
|
valuesContent: |-
|
||||||
|
ports:
|
||||||
|
tcpechoa:
|
||||||
|
port: 9098
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
exposedPort: 9098
|
||||||
|
protocol: TCP
|
||||||
|
tcpechob:
|
||||||
|
port: 9099
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
exposedPort: 9099
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-a
|
||||||
|
namespace: tcp-echo
|
||||||
|
labels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: a
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: a
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: a
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: tcp-echo
|
||||||
|
image: gitea.eitczech.com/public/tcp-echo-server:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "9098"
|
||||||
|
- name: INSTANCE_ID
|
||||||
|
value: "echo-a"
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
containerPort: 9098
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: tcp
|
||||||
|
initialDelaySeconds: 2
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: tcp
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 20m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1654
|
||||||
|
runAsGroup: 1654
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-a
|
||||||
|
namespace: tcp-echo
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: a
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
port: 9098
|
||||||
|
targetPort: tcp
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-a
|
||||||
|
namespace: tcp-echo
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- tcpechoa
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`*`)
|
||||||
|
services:
|
||||||
|
- name: tcp-echo-a
|
||||||
|
port: 9098
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-b
|
||||||
|
namespace: tcp-echo
|
||||||
|
labels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: b
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: b
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: b
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: tcp-echo
|
||||||
|
image: gitea.eitczech.com/public/tcp-echo-server:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "9099"
|
||||||
|
- name: INSTANCE_ID
|
||||||
|
value: "echo-b"
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
containerPort: 9099
|
||||||
|
protocol: TCP
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: tcp
|
||||||
|
initialDelaySeconds: 2
|
||||||
|
periodSeconds: 5
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: tcp
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 20m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 1654
|
||||||
|
runAsGroup: 1654
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-b
|
||||||
|
namespace: tcp-echo
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: tcp-echo
|
||||||
|
instance: b
|
||||||
|
ports:
|
||||||
|
- name: tcp
|
||||||
|
port: 9099
|
||||||
|
targetPort: tcp
|
||||||
|
protocol: TCP
|
||||||
|
---
|
||||||
|
apiVersion: traefik.io/v1alpha1
|
||||||
|
kind: IngressRouteTCP
|
||||||
|
metadata:
|
||||||
|
name: tcp-echo-b
|
||||||
|
namespace: tcp-echo
|
||||||
|
spec:
|
||||||
|
entryPoints:
|
||||||
|
- tcpechob
|
||||||
|
routes:
|
||||||
|
- match: HostSNI(`*`)
|
||||||
|
services:
|
||||||
|
- name: tcp-echo-b
|
||||||
|
port: 9099
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- 00-namespace.yaml
|
||||||
|
- 30-traefik-entrypoint.yaml
|
||||||
|
- instance-a.yaml
|
||||||
|
- instance-b.yaml
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# For non-k3s installs: helm upgrade traefik traefik/traefik -n <traefik-ns> --reuse-values -f k8s/traefik-values.yaml
|
||||||
|
ports:
|
||||||
|
tcpechoa:
|
||||||
|
port: 9098
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
exposedPort: 9098
|
||||||
|
protocol: TCP
|
||||||
|
tcpechob:
|
||||||
|
port: 9099
|
||||||
|
expose:
|
||||||
|
default: true
|
||||||
|
exposedPort: 9099
|
||||||
|
protocol: TCP
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
var port = int.TryParse(Environment.GetEnvironmentVariable("PORT"), out var p) ? p : 9099;
|
||||||
|
var instanceId = Environment.GetEnvironmentVariable("INSTANCE_ID") is { Length: > 0 } id
|
||||||
|
? id
|
||||||
|
: Environment.MachineName;
|
||||||
|
|
||||||
|
var listener = new TcpListener(IPAddress.Any, port);
|
||||||
|
listener.Start();
|
||||||
|
Console.WriteLine($"TCP echo server '{instanceId}' listening on 0.0.0.0:{port}");
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel();
|
||||||
|
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var client = await listener.AcceptTcpClientAsync(cts.Token);
|
||||||
|
_ = HandleClientAsync(client, instanceId, cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
listener.Stop();
|
||||||
|
Console.WriteLine("Shutting down.");
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task HandleClientAsync(TcpClient client, string instanceId, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var remote = client.Client.RemoteEndPoint?.ToString() ?? "unknown";
|
||||||
|
Console.WriteLine($"Connected: {remote}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (client)
|
||||||
|
{
|
||||||
|
var stream = client.GetStream();
|
||||||
|
|
||||||
|
// Greeting so the caller knows which instance it reached even before sending anything.
|
||||||
|
await stream.WriteAsync(Encoding.UTF8.GetBytes($"[{instanceId}] connected\n"), ct);
|
||||||
|
|
||||||
|
var prefix = Encoding.UTF8.GetBytes($"[{instanceId}] ");
|
||||||
|
var buffer = new byte[4096];
|
||||||
|
var reply = new byte[prefix.Length + buffer.Length];
|
||||||
|
prefix.CopyTo(reply, 0);
|
||||||
|
int read;
|
||||||
|
while ((read = await stream.ReadAsync(buffer, ct)) > 0)
|
||||||
|
{
|
||||||
|
buffer.AsSpan(0, read).CopyTo(reply.AsSpan(prefix.Length));
|
||||||
|
await stream.WriteAsync(reply.AsMemory(0, prefix.Length + read), ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or SocketException or OperationCanceledException)
|
||||||
|
{
|
||||||
|
// client went away or shutdown requested
|
||||||
|
}
|
||||||
|
Console.WriteLine($"Disconnected: {remote}");
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Connects to a tcp-echo instance, prints the greeting, sends a message and prints the reply.
|
||||||
|
.EXAMPLE
|
||||||
|
.\test-echo.ps1 -Server sparkyxf.cz -Port 9098
|
||||||
|
.\test-echo.ps1 -Server sparkyxf.cz -Port 9099 -Message "ping"
|
||||||
|
#>
|
||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true, Position = 0)]
|
||||||
|
[string]$Server,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $true, Position = 1)]
|
||||||
|
[ValidateRange(1, 65535)]
|
||||||
|
[int]$Port,
|
||||||
|
|
||||||
|
[string]$Message = "hello",
|
||||||
|
|
||||||
|
[int]$TimeoutMs = 5000
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Read-Line {
|
||||||
|
param([System.IO.StreamReader]$Reader)
|
||||||
|
$line = $Reader.ReadLine()
|
||||||
|
if ($null -eq $line) { throw "Connection closed by server." }
|
||||||
|
return $line
|
||||||
|
}
|
||||||
|
|
||||||
|
$client = New-Object System.Net.Sockets.TcpClient
|
||||||
|
try {
|
||||||
|
$client.ReceiveTimeout = $TimeoutMs
|
||||||
|
$client.SendTimeout = $TimeoutMs
|
||||||
|
|
||||||
|
Write-Host "Connecting to ${Server}:${Port} ..." -ForegroundColor Cyan
|
||||||
|
if (-not $client.ConnectAsync($Server, $Port).Wait($TimeoutMs)) {
|
||||||
|
throw "Connect to ${Server}:${Port} timed out after ${TimeoutMs} ms."
|
||||||
|
}
|
||||||
|
|
||||||
|
$stream = $client.GetStream()
|
||||||
|
$reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8)
|
||||||
|
$writer = New-Object System.IO.StreamWriter($stream, (New-Object System.Text.UTF8Encoding($false)))
|
||||||
|
$writer.NewLine = "`n"
|
||||||
|
$writer.AutoFlush = $true
|
||||||
|
|
||||||
|
Write-Host "<- $(Read-Line $reader)" -ForegroundColor Green # greeting: [instance] connected
|
||||||
|
Write-Host "-> $Message"
|
||||||
|
$writer.WriteLine($Message)
|
||||||
|
Write-Host "<- $(Read-Line $reader)" -ForegroundColor Green # echo: [instance] <message>
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
$client.Dispose()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user