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
    }
}