TCPソケットを使用してHTTP通信を仲介する常駐アプリ
![](https://www.pxp.co.jp/wp-content/uploads/2019/03/PGM2.jpg)
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 } }