TCPソケットを使用してHTTP通信を仲介する常駐アプリ

C#を使用してこんな機能をつくってみました。
TCPソケットを使用してHTTP通信を仲介する常駐アプリ。
実際にはサービスプログラムを作成して
Windowsサービスとして常駐させてます。
こんな感じでで、動作させます。
ブラウザ⇔仲介ソフト⇔HTTPサーバ
①.http://localhost:起動ポート/hogehoge
②.http://HTTPサーバ/hogehoge
ブラウザで①にアクセスすると
②にアクセスする様になってます。
残念ながら①のURLはHTTPS禁止。。。
あと、HTTPヘッダはUTF-8決め打ちになってます。
呼出しはこんな感じ
TcpProgram _tcpProg = new TcpProgram(“20202”);
_tcpProg.Init();
終了はこんな感じ
_tcpProg.Stop();
納品するソースから抜粋したので真面目に作ってあります。
必要に応じてみてみてください。
(個人的にはC#は初挑戦なので、お作法が悪かったらすみません。)
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
namespace HogeFugeApp
{
/// <summary>
/// TCP送受信制御クラス
/// </summary>
internal class TcpProgram
{
#region クラス内変数
/// <summary>
/// プロセス停止処理中フラグ
/// </summary>
private static bool b_isStopping = false;
/// <summary>
/// 通知時に手動でリセットする必要のあるスレッド同期イベント
/// </summary>
private static readonly ManualResetEvent _socketEvent = new ManualResetEvent(false);
/// <summary>
/// IPアドレス、ポートでネットワークエンドポイント示すクラス
/// </summary>
private readonly IPEndPoint _ipEndPoint;
/// <summary>
/// ソケットクラス
/// </summary>
private Socket _sock;
/// <summary>
/// スレッドクラス
/// </summary>
private Thread _mainThread;
/// <summary>
/// HTTPヘッダ【Content-Type】
/// </summary>
public const string HTTP_HEADER_CONTENT_TYPE = "Content-Type";
/// <summary>
/// HTTPヘッダ【Content-Length】
/// </summary>
public const string HTTP_HEADER_CONTENT_LENGTH = "Content-Length";
/// <summary>
/// HTTPヘッダ【Cookie】
/// </summary>
public const string HTTP_HEADER_COOKIE = "Cookie";
#endregion
#region TCP制御
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="regVals">レジストリ値を格納したMAP</param>
public TcpProgram(int port)
{
string ipString = "127.0.0.1";
IPAddress myIp = IPAddress.Parse(ipString);
_ipEndPoint = new IPEndPoint(myIp, port);
}
/// <summary>
/// 初期化処理
/// </summary>
public void Init()
{
_sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_sock.Bind(_ipEndPoint);
int maxThreds = 10;
_sock.Listen(maxThreds);
_mainThread = new Thread(new ThreadStart(Round));
_mainThread.Start();
}
/// <summary>
/// TCP受信待ち受け開始処理
/// </summary>
public void Round()
{
while (true)
{
_socketEvent.Reset();
_sock.BeginAccept(new AsyncCallback(ConnectRequest), _sock);
_socketEvent.WaitOne();
}
}
/// <summary>
/// TCP受信待ち受け終了処理
/// </summary>
public void Stop()
{
b_isStopping = true;
_sock.Close();
}
/// <summary>
/// TCP受信時処理
/// </summary>
/// <param name="ar">非同期操作のステータス</param>
public void ConnectRequest(IAsyncResult ar)
{
try
{
_socketEvent.Set();
Socket listener = (Socket)ar.AsyncState;
// 停止中にConnectRequestが動いちゃってエラーになるから停止中はreturnしちゃう様にした
if (b_isStopping)
{
return;
}
Socket handler = listener.EndAccept(ar);
StateObject state = new StateObject
{
WorkSocket = handler
};
handler.BeginReceive(state._buffer, 0, StateObject.BUFFER_SIZE, 0, new AsyncCallback(ReadCallback), state);
}
catch (Exception e)
{
throw e;
}
}
/// <summary>
/// TCP受信バッファ読込時処理
/// </summary>
/// <param name="ar">非同期操作のステータス</param>
public async void ReadCallback(IAsyncResult ar)
{
StateObject state = null;
try
{
state = (StateObject)ar.AsyncState;
Socket handler = state.WorkSocket;
int readSize = handler.EndReceive(ar);
if (readSize < 1)
{
return;
}
// TCP受信が完了したかをチェックする。
if (!state.IsEndReceive(readSize))
{
// 未完了の場合は継続してTCP受信する
handler.BeginReceive(state._buffer, 0, StateObject.BUFFER_SIZE, 0, new AsyncCallback(ReadCallback), state);
return;
}
byte[] responseBuuffer;
responseBuuffer = await RelayAsync(state);
// 結果をTCPソケットとして返却する
handler.BeginSend(responseBuuffer, 0, responseBuuffer.Length, 0, new AsyncCallback(WriteCallback), state);
}
catch (Exception e)
{
if (!(state is null))
{
state.WorkSocket.Close();
}
}
}
/// <summary>
/// HTTP連携を行う
/// </summary>
/// <param name="state">TCP受信情報格納クラス</param>
/// <returns>連携の結果のレスポンス</returns>
private async Task<byte[]> RelayAsync(StateObject state)
{
try
{
// URLを取得する
string tmpUrl = "https://どっかのサイト" + state._httpAction;
// HTTPクライアントヘッダを生成
HttpClientHandler handler = new HttpClientHandler
{
AllowAutoRedirect = false // 自動リダイレクトOFF
};
// cookieをセット
string cookieCont = state.GetHttpHeader(HTTP_HEADER_COOKIE);
SetCookie(cookieCont, handler, tmpUrl);
// 連携のためのHTTPリクエストを生成
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(state._httpMethod), tmpUrl);
// HTTPリクエストボディがある場合はHTTPリクエストにContent-TypeとHTTPリクエストボディをセット
if (state._httpBody.Length > 0)
{
request.Content = new ByteArrayContent(state._httpBody);
string contentType = state.GetHttpHeader(HTTP_HEADER_CONTENT_TYPE);
if (contentType != "")
{
request.Content.Headers.Add(HTTP_HEADER_CONTENT_TYPE, contentType);
}
}
// HTTP送受信を行う
using (HttpClient client = new HttpClient(handler))
{
// HTTPレスポンスを取得する
HttpResponseMessage response = await client.SendAsync(request);
}
// 結果の改行コードを取得する
byte[] tmpSep = state.GetLineSeparator(Encoding.UTF8.GetBytes(response.Headers.ToString()));
// HTTPバージョン + HTTPステータスコード + 文言を設定
byte[] resStatus = Encoding.UTF8.GetBytes(state._httpVersion + " "
+ response.StatusCode.GetHashCode().ToString() + " "
+ response.StatusCode.ToString());
// 最初のHTTPレスポンスからContent-Typeを取得
byte[] contType = Encoding.UTF8.GetBytes(HTTP_HEADER_CONTENT_TYPE + ": "
+ response.Content.Headers.GetValues(HTTP_HEADER_CONTENT_TYPE).ToArray()[0]);
// 最後のHTTPレスポンスからContent-Lengthを取得
byte[] contLen = Encoding.UTF8.GetBytes((HTTP_HEADER_CONTENT_LENGTH + ": "
+ response.Content.Headers.GetValues(HTTP_HEADER_CONTENT_LENGTH).ToArray()[0]));
// 最初のHTTPレスポンスからその他のヘッダを取得
byte[] resHeader = Encoding.UTF8.GetBytes(response.Headers.ToString());
// 最後のHTTPレスポンスからボディを取得
byte[] resBody = await response.Content.ReadAsByteArrayAsync();
// 結果をセットする
byte[] result = new byte[resStatus.Length + +contType.Length + contLen.Length + resHeader.Length + resBody.Length + (tmpSep.Length * 4)];
// HTTPバージョン + HTTPステータスコード + 文言を設定
int start = 0;
Array.Copy(resStatus, 0, result, start, resStatus.Length);
start += resStatus.Length;
// 改行コード
Array.Copy(tmpSep, 0, result, start, tmpSep.Length);
start += tmpSep.Length;
// Content-Type
Array.Copy(contType, 0, result, start, contType.Length);
start += contType.Length;
// 改行コード
Array.Copy(tmpSep, 0, result, start, tmpSep.Length);
start += tmpSep.Length;
// Content-Length
Array.Copy(contLen, 0, result, start, contLen.Length);
start += contLen.Length;
// 改行コード
Array.Copy(tmpSep, 0, result, start, tmpSep.Length);
start += tmpSep.Length;
// その他のヘッダ
Array.Copy(resHeader, 0, result, start, resHeader.Length);
start += resHeader.Length;
// 改行コード
// (その他のヘッダはToStringで取得しているので改行コードが1つ含まれている
// そのため、本来はボディとヘッダの間の改行コードは2つ必要だが、
// 1回追加するだけでよい)
Array.Copy(tmpSep, 0, result, start, tmpSep.Length);
start += tmpSep.Length;
// ボディ
Array.Copy(resBody, 0, result, start, resBody.Length);
return result;
}
catch (Exception e)
{
return Encoding.UTF8.GetBytes(
state._httpVersion + " "
+ HttpStatusCode.ServiceUnavailable.GetHashCode().ToString() + " "
+ HttpStatusCode.ServiceUnavailable.ToString()
);
}
}
/// <summary>
/// HTTPハンドラにCookieをセットする
/// </summary>
/// <param name="cookieCont">Cookie</param>
/// <param name="handler">HTTPハンドラ</param>
/// <param name="tmpUrl">URL</param>
private void SetCookie(string cookieCont, HttpClientHandler handler, string tmpUrl)
{
// Cookieがないときは何もしない
if (cookieCont == "")
{
return;
}
// cookieのパスをセットする
string cookiePath = "";
// http://やhttps://の次の最初の位置を取得する
int posPrefix = tmpUrl.IndexOf("://") + 3;
// /があればその位置 + 1バイトを取得
int posPathStart = tmpUrl.IndexOf('/', posPrefix);
// URLに/が含まれる場合は、その部分をパスとして取得する
if (posPathStart > 0)
{
cookiePath = tmpUrl.Substring(posPathStart, tmpUrl.Length - posPathStart);
}
// cookieはkey1=val1;key2=val2になっているので最初に;で分割
string[] cookies = cookieCont.Split(';');
Boolean isFirstRow = true;
foreach (string cookie in cookies)
{
// 1つ1つのcookieをkeyとvalに分割する
int posEq = cookie.IndexOf('=');
if (posEq == -1)
{
continue;
}
string key = cookie.Substring(0, posEq);
string val = cookie.Substring(posEq + 1);
// 最初のCookieの設定の時だけURLを一緒に設定
if (isFirstRow)
{
handler.CookieContainer.Add(new Uri(tmpUrl), new Cookie(key, val, cookiePath));
isFirstRow = false;
}
else
{
handler.CookieContainer.Add(new Cookie(key, val, cookiePath));
}
}
}
/// <summary>
/// TCP送信時処理
/// </summary>
/// <param name="ar">非同期操作のステータス</param>
public void WriteCallback(IAsyncResult ar)
{
try
{
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.WorkSocket;
handler.EndSend(ar);
state.WorkSocket.Close();
}
catch (Exception e)
{
throw e;
}
}
#endregion
#region TCP受信情報格納クラス
/// <summary>
/// TCP受信情報格納クラス
/// </summary>
private class StateObject
{
/// <summary>
/// ソケット
/// </summary>
public Socket WorkSocket { get; set; }
/// <summary>
/// 1度のTCP受信における最大バッファサイズ
/// </summary>
public const int BUFFER_SIZE = 1024;
/// <summary>
/// TCP受信バッファ
/// </summary>
internal byte[] _buffer = new byte[BUFFER_SIZE];
/// <summary>
/// 1度のHTTPリクエストにおける受信バッファ
/// 1度のTCP受信は最大バッファサイズまでなので
/// ここに累積して保持する
/// </summary>
internal byte[] _allBuffer = new byte[0];
/// <summary>
/// HTTPヘッダの改行コード
/// </summary>
internal byte[] _lineSeprater = new byte[0];
/// <summary>
/// 複数回のTCP受信があった場合に、2回目以降の
/// 受信完了チェックをより速やかに行うため、前回の
/// 受信完了チェックで検索した位置を保持しておく
/// </summary>
internal int _startIndex = 0;
/// <summary>
/// HTTPリクエストヘッダ
/// </summary>
internal byte[] _httpHeader = new byte[0];
/// <summary>
/// HTTPリクエストヘッダのメソッド
/// </summary>
internal string _httpMethod = "";
/// <summary>
/// HTTPリクエストヘッダのアクション
/// </summary>
internal string _httpAction = "";
/// <summary>
/// HTTPリクエストヘッダのバージョン
/// </summary>
internal string _httpVersion = "";
/// <summary>
/// HTTPリクエストボディ
/// </summary>
internal byte[] _httpBody = new byte[0];
/// <summary>
/// TCP受信が完了したか否かを判定する。
/// 処理の過程で、TCP受信バッファを分解し
/// 下記の項目を取得する
/// ・HTTPヘッダの改行コード
/// ・HTTPリクエストヘッダ
/// ・HTTPリクエストボディ
/// </summary>
/// <param name="readSize">今回の受信バイト数</param>
/// <returns>TCP受信が完了した場合、true/未完了の場合、false</returns>
public Boolean IsEndReceive(int readSize)
{
// 今回のTCP受信バッファを全体のTCP受信バッファに追加する
AppendAllBuffer(readSize);
// HTTPヘッダの改行コードが未取得の場合、最初の改行コードをTCP受信バッファから取得する
if (_lineSeprater.Length == 0)
{
// TCP受信バッファから改行コードを検索する
byte[] tmpSep = GetLineSeparator(_allBuffer);
// 改行コードが見つからなかった場合は、受信未完了を返却する
if (tmpSep.Length == 0)
{
return false;
}
// 改行コードが見つかった場合は、改行コードをHTTPヘッダの改行コードとして取得して
// 処理を継続する
_lineSeprater = tmpSep;
}
// HTTPヘッダとHTTPボディの間には改行コードが2つ入っているので、それの検索のために
// HTTPヘッダ/ボディの切れ目として連続する改行コードを作成しておく
byte[] sepHeadAndBody = new byte[_lineSeprater.Length * 2];
for (int i = 0; i < 2; i++)
{
Array.Copy(_lineSeprater, 0, sepHeadAndBody, _lineSeprater.Length * i, _lineSeprater.Length);
}
// HTTPリクエストヘッダが未取得の場合、TCP受信バッファからHTTPヘッダ/ボディの切れ目を検索し、その手前を
// HTTPリクエストヘッダとして取得する
if (_httpHeader.Length == 0)
{
// HTTPヘッダ/ボディの切れ目の位置を取得する
int posSepHeadAndBody = ByteArrayIndexOf(_allBuffer, sepHeadAndBody, _startIndex);
// HTTPヘッダ/ボディの切れ目が見つからない場合、次回の検索のために既に検索した位置を
// 現在受信しているTCP受信バッファの長さ - HTTPヘッダ/ボディの切れ目の長さで更新して
// 受信未完了を返却する
if (posSepHeadAndBody < 0)
{
_startIndex = _allBuffer.Length - sepHeadAndBody.Length;
return false;
}
// HTTPヘッダ/ボディの切れ目が見つかった場合、HTTPリクエストヘッダを取得する
byte[] tmpHead = new byte[posSepHeadAndBody];
Array.Copy(_allBuffer, tmpHead, posSepHeadAndBody);
_httpHeader = tmpHead;
// メソッド、アクション、バージョンを取得してプロパティにセットしておく
SetMethodAndActionVersionToProp();
}
// HTTPヘッダのContent-Lengthを取得する
string tmpContenLength = GetHttpHeader(TcpProgram.HTTP_HEADER_CONTENT_LENGTH);
// メソッドがGETの場合など、ボディがない場合は、Content-Lengthもセットされないため
// その場合はゼロとして扱う
int contenLength = 0;
if (tmpContenLength != "")
{
contenLength = Int32.Parse(tmpContenLength);
}
// TCP受信バッファの長さ - HTTPヘッダの長さ - HTTPヘッダ/ボディの切れ目の長さ≠Content-Lengthの場合
// ボディの受信が完了していないので、受信未完了を返却する
if (_allBuffer.Length - _httpHeader.Length - sepHeadAndBody.Length != contenLength)
{
return false;
}
// ボディの受信が完了した場合、ボディを取得し、受信完了を返却する
byte[] tmpBody = new byte[contenLength];
Array.Copy(_allBuffer, _httpHeader.Length + sepHeadAndBody.Length, tmpBody, 0, contenLength);
_httpBody = tmpBody;
return true;
}
/// <summary>
/// 今回のTCP受信バッファを全体のTCP受信バッファに追加する
/// </summary>
/// <param name="readSize">今回の受信バイト数</param>
private void AppendAllBuffer(int readSize)
{
byte[] allBuf = new byte[_allBuffer.Length + readSize];
Array.Copy(_allBuffer, allBuf, _allBuffer.Length);
Array.Copy(_buffer, 0, allBuf, _allBuffer.Length, readSize);
_allBuffer = allBuf;
}
/// <summary>
/// TCP受信バッファから改行コードを取得する
/// </summary>
/// <param name="buffer">TCP受信バッファ</param>
/// <returns>改行コード</returns>
public byte[] GetLineSeparator(byte[] buffer)
{
// 最初CRの位置を取得
int crIndex = ByteArrayIndexOf(buffer, new byte[1] { 13 }, 0);
// 最初LFの位置を取得
int lfIndex = ByteArrayIndexOf(buffer, new byte[1] { 10 }, 0);
// CR LF両方が見つからない場合は、0バイトの配列を返却する
if ((crIndex < 0) && (lfIndex < 0))
{
return new byte[0];
}
// CRの最初の位置がLFの位置の1バイト前の場合
if (crIndex == lfIndex - 1)
{
// CR LFを返却
return new byte[2] { 13, 10 };
}
// CRの方が前の場合、戻り値にCRを返却
if (crIndex < lfIndex)
{
return new byte[1] { 13 };
}
// その他の場合、戻り値にLFを返却
else
{
return new byte[1] { 10 };
}
}
/// <summary>
/// HTTPヘッダの項目を取得する
/// </summary>
/// <param name="key">取得する項目のキー</param>
/// <returns>取得した項目の値</returns>
public string GetHttpHeader(string key)
{
int start = 0;
string result = "";
// 検索開始位置がHTTPヘッダの長さを超えるまでループ
while (_httpHeader.Length > start)
{
// 改行コードの位置を取得する。取得できない場合は最後の項目なので
// HTTPヘッダの長さ - 1を改行コードの位置に設定する
int end = ByteArrayIndexOf(_httpHeader, _lineSeprater, start);
if (end == -1)
{
end = _httpHeader.Length - 1;
}
// HTTPヘッダの1行分を取得し、文字列化する
byte[] tmpLine = new byte[end - start];
Array.Copy(_httpHeader, start, tmpLine, 0, end - start);
string tmpCont = Encoding.UTF8.GetString(tmpLine).Trim();
// 受け渡されたキーと一致する場合
if (tmpCont.ToLower().IndexOf(key.ToLower()) == 0)
{
// :の位置を取得し、その後を値として取得する
int posSemiColon = tmpCont.IndexOf(":");
if (posSemiColon > 0)
{
result = tmpCont.Substring(posSemiColon + 1).TrimStart();
break;
}
}
// 一致しなければ、開始位置を検索済の位置までスライドして次の検索
start = end + _lineSeprater.Length;
}
// 結果を返却する
return result;
}
/// <summary>
/// HTTPヘッダからメソッドとアクション、バージョンを取得し
/// プロパティに設定する
/// </summary>
private void SetMethodAndActionVersionToProp()
{
// HTTPリクエストの1行目を取得する
int idx = ByteArrayIndexOf(_httpHeader, _lineSeprater, 0);
if (idx == -1)
{
idx = _httpHeader.Length;
}
byte[] TmpMethodAndActionVersion = new byte[idx];
Array.Copy(_allBuffer, TmpMethodAndActionVersion, idx);
// メソッド、アクション、バージョンの順に配列に入る
string[] MethodAndActionVersion = Encoding.UTF8.GetString(TmpMethodAndActionVersion).Split(' ');
_httpMethod = MethodAndActionVersion[0];
_httpAction = MethodAndActionVersion[1];
_httpVersion = MethodAndActionVersion[2];
}
/// <summary>
/// 全体のバイトの配列から、検索対象のバイトの配列が
/// 最初にみつかる位置を取得する
/// </summary>
/// <param name="target">全体のバイトの配列</param>
/// <param name="pattern">検索対象のバイトの配列</param>
/// <param name="start">検索開始位置</param>
/// <returns>位置</returns>
private int ByteArrayIndexOf(byte[] target, byte[] pattern, int start)
{
// 検索開始位置から全体のバイトの配列の長さ - 検索対象のバイトの
// 配列の長さまでをループ + 1バイトまでをループ
for (int i = start; i < target.Length - pattern.Length + 1; i++)
{
Boolean matched = true;
// 検索対象のバイトの配列を全てループ
for (int j = 0; j < pattern.Length; j++)
{
// 全体のバイトの配列の値と検索対象のバイトの配列の値が
// 一致するかチェック
if (target[i + j] != pattern[j])
{
matched = false;
break;
}
}
// 全ての値が一致した場合、最初の位置を返却する
if (matched)
{
return i;
}
}
// 1回も一致しなければ-1を返却する
return -1;
}
}
#endregion
}
}


