Linux - セクション 16 - ネットワークの基礎 (アプリケーション層 2)

目次

1.HTTPプロトコル

1.1. HTTP の概要

1.2. URL について

1.3. urlencode と urldecode

1.4.HTTPプロトコル形式

1.4.1.HTTPリクエストプロトコルの形式

1.4.2.HTTP応答プロトコルの形式

1.4.3. HTTP リクエストプロトコル形式の確認

1.4.4. HTTP 応答プロトコル形式の確認

1.5. サーバー側の HTTP 応答コードの実装

1.6. HTTPメソッド

1.6.1. クライアントアップロードデータのコード例

1.6.2. GETメソッドとPOSTメソッド

1.7. HTTPステータスコード

1.7.1. HTTP ステータスコードの概要

1.7.2.リダイレクトリダイレクトステータスコード

1.7.3. リダイレクト リダイレクト ステータス コード コードの使用法

1.8.HTTP共通ヘッダリクエスト/レスポンスヘッダ

1.8.1. HTTP 共通ヘッダーのリクエスト/レスポンスヘッダーの概要

1.8.2.クッキー

1.8.3.セッション


1.HTTPプロトコル

ソケット部分に記述したコードとアプリケーション層 1 に記述したネットワーク計算コードは、どちらもアプリケーション層で動作し、アプリケーション層のコードであるため、アプリケーション層のコードには次のものが含まれます。

(1) 基本システムソケットソケットシリーズインターフェースの使用。

(2) カスタマイズされたプロトコル (Network Calculator Protocol.hpp ファイル内のコードなど)

(3) ビジネスの記述(ネットワーク計算機のseverTcpファイルに計算機インターフェースなど)

注: プロトコルのカスタマイズとサービスの作成は 2 つの異なるものであり、切り離すことができます。

ネットワーク計算機のシナリオでは、ソケット シリーズ インターフェイスの使用、プロトコルのカスタマイズ、ビジネスの記述のプロセスが面倒であり、考慮事項は必ずしも包括的ではありません。プログラマは、一般的に使用されるいくつかのシナリオに合わせて成熟したプロトコルをカスタマイズしており、これらのプロトコルは徐々にアプリケーション層の特定のプロトコルの標準となり、私たちが直接使用できるようになりました。

アプリケーション層コードのカスタムプロトコル部分の一般的なプロトコル規格としては、http、https、smtp、ftp、DNSなどが挙げられます。ここでは主にhttpとhttpsについて説明します。

注: これらの共通プロトコル標準は、主に、アプリケーション層コードの「基本システム ソケット ソケット シリーズ インターフェイスの使用」と「カスタム プロトコル」の機能を完了します。

1.1. HTTP の概要

HTTP (ハイパー テキスト転送プロトコル) プロトコルは、ハイパーテキスト転送プロトコルとも呼ばれ、単純な要求と応答のプロトコルであり、通常は TCP 上で実行されます。

ネットワーク通信コードを記述するとき、プロトコルを自分でカスタマイズできますが、実際には、多くの優秀なエンジニアがすでに多くの非常に成熟したアプリケーション層プロトコルを作成しています。その最も典型的なものは HTTP プロトコルです。

1.2. URL について

URL (Uniform Resource Lacator) はユニフォーム リソース ロケーターと呼ばれ、通常 Web アドレスと呼ばれるもので、インターネットの World Wide Web サービス プログラム上の情報の所在を指定するために使用される表現方法です。

URL は大まかに次の部分で構成されます。

プロトコルスキーム名:

http:// はプロトコルの名前を示し、リクエストで使用されるプロトコルを示します。通常は HTTP プロトコルまたはセキュリティ プロトコル HTTPS が使用されます。HTTPS はセキュリティ指向の HTTP チャネルであり、HTTP に基づく通信の暗号化と本人認証によって通信プロセスのセキュリティを確保します。

一般的なアプリケーション層プロトコル:

\cdot DNS (Domain Name System) プロトコル: ドメイン ネーム システム。
\cdot FTP (File Transfer Protocol) プロトコル: ファイル転送プロトコル。
\cdot TELNET (テルネット) プロトコル: リモート端末プロトコル。
\cdot HTTP (ハイパーテキスト転送プロトコル) プロトコル: ハイパーテキスト転送プロトコル。
\cdot HTTPS (Hyper Text Transfer Protocol over SecureSocket Layer) プロトコル: 安全なデータ伝送プロトコル。
\cdot SMTP(Simple Mail Transfer Protocol)プロトコル:電子メール送信プロトコル。
\cdot POP3 (Post Office Protocol - バージョン 3) プロトコル: メール読み取りプロトコル。
\cdot SNMP (簡易ネットワーク管理プロトコル): 簡易ネットワーク管理プロトコル。
\cdot TFTP (Trivial File Transfer Protocol) プロトコル: 単純なファイル転送プロトコル。

ログイン情報:

usr:pass は、ログインユーザーのユーザー名とパスワードなどのログイン認証情報を示します。ログイン認証情報は URL に反映できますが、ログイン情報は他の方法 (Web ページ スキャン コード ログイン、Web ページ アカウント パスワード ログインなど) を通じてサーバーに配信できるため、ほとんどの URL ではこのフィールドは省略されます。

サーバー アドレス:
www.example.jp は、www.alibaba.com、www.qq.com、www.baidu.com など、ドメイン名とも呼ばれるサーバー アドレスを表します。

パブリック ネットワーク内のホストを識別するために IP アドレスを使用しますが、IP アドレス自体はユーザーが見るのに適していないことに注意してください。たとえば、ping コマンドを使用して、www.baidu.com と www.qq.com の解決された IP アドレスをそれぞれ取得できます。

ユーザーがこれら 2 つの IP アドレスを見た場合、ユーザーはこの Web サイトにアクセスするまで 2 つの Web サイトが何をしているのか知りませんが、www.baidu.com と www.qq.com の 2 つのドメイン名が表示された場合、ユーザーは少なくとも2 つの Web サイトがどの会社に対応しているかがわかるため、ドメイン名がより適切に自己記述できるようになります。 

実はドメイン名とIPアドレスは同等のものと考えることができ、コンピュータで利用する場合にはドメイン名とIPアドレスの両方を使用することができます。ただし、URL はユーザーが見ることができるため、サーバー アドレスは URL 内でドメイン名の形式で表されます。

注: ドメイン名は最終的に IP アドレスに変換されます。

サーバーのポート番号:

80 はサーバーのポート番号を表します。HTTP プロトコルは、ソケット プログラミングと同様、アプリケーション層にあります。ソケット プログラミングを実行する場合、対応する IP とポートをサーバーにバインドする必要があり、ここでのアプリケーション層プロトコルにも明確なポート番号が必要です。

一般的なプロトコルに対応するポート番号:

プロトコル名 対応ポート番号
HTTP 80
HTTPS 443
SSH 22

私たちが特定のプロトコルを使用するとき、そのプロトコルは実際にサービスを提供していますが、これらの一般的に使用されるサービスとポート番号の対応は明確になっているため、実際には特定のプロトコルを使用するときにポート番号を指定する必要はありません。番号がプロトコルに対応すると、ブラウザは対応するポート番号を自動的に追加するため、通常、サーバーのポート番号は URL では省略されます。

ノート:

1. ネットワーク通信の本質はソケット通信であり、ソケット通信には IP アドレス + ポート番号が必要であるため、クライアントはサーバーにアクセスするときにドメイン名 (IP アドレス) とポート番号を持っている必要があります。

2. 前述したように、ポート番号 0 ~ 1023 はこれらの標準プロトコルで使用されるため、テスト時には 1024 より前のポート番号をバインドしないでください。

階層ファイル パス:
/dir/index.htm は、アクセスするリソースが配置されているパスを表します。サーバーにアクセスする目的は、サーバー上の特定のリソースを取得することです。対応するサーバー プロセスは、前のドメイン名とポートを通じてすでに見つかります。この時点で行う必要があるのは、リソースが配置されているパスを指定することです。 。

たとえば、ブラウザを開いて Baidu のドメイン名を入力すると、下の図 1 に示すように、ブラウザは Baidu の Web ページのホームページを取得するのに役立ちます。Web ページ リクエストを開始するとき、基本はサーバーの特定の .html Web ページ ファイルにアクセスし、図 23 に示すように Web ページ情報を .html ファイルからローカルに取得することです (図 23 はそれぞれ Web ページと Linux を示しています)。 Baidu Web ページのホームページ上のファイル情報を取得する方法)、ブラウザがこの Web ページ上の情報を解釈し、最終的に対応する Web ページを表示するこのリソースを Web ページ リソースと呼ぶことができます。

さらに、ビデオ、オーディオ、Web ページ、画像などのリソースもサーバーに要求します。HTTP が Text Transfer Protocol ではなく Hypertext Transfer Protocol と呼ばれる理由は、実際には通常のテキスト リソースではないリソースが多数あるためです。

したがって、URL にはそのようなフィールドがあり、アクセスするリソースが存在するパスを示すために使用されます。さらに、ここではパス区切り文字が \ ではなく / であることがわかります。これは、多くのサービスが実際に Linux にデプロイされていることを証明しています。
ノート:

1. http プロトコルの機能は、表示または何らかの使用のためにローカルで取得するために、特定のサーバーから特定の「リソース」を申請することです。

2. クライアント ホスト (ハードウェア) のクライアント プロセス クライアント (ソフトウェア) がリソースを取得しない場合、リソースはどこにありますか? リソースは、対応する Web サーバー プロセス サーバー (ソフトウェア) が配置されているサーバー (ハードウェア) 上にあります。

3. クライアントからサーバーに要求されたすべてのビデオ、オーディオ、Web ページ、画像、その他のリソースはファイルです。これらのリソース ファイルは Linux サーバー上にあり、サーバーの対応するプロセスがサーバー上の対応するファイルを開いて読み取ります。そしてそれを要求元のクライアントプロセスに送信します。

サーバーの対応するプロセスは、クライアント要求に対応するファイルを開いて読み取る必要があります。前提条件は、ファイルが見つかることです。Linux サーバーはパスを通じてファイルを見つけるため、階層を持つファイル パス フィールドが必要です。 URLにあります。

4. 階層化されたファイル パスの先頭の / は、必ずしもルート ディレクトリを表すとは限りません。アクセス サーバーの特定のファイル リソースには、絶対パスまたは相対パスとしてアクセスできます。一般に、先頭の / はルート ディレクトリではありません。 、特定のパスでアクセスされます。

例: クライアントがリクエストした場合、URL 内の階層ファイルのパスは /dxf/test/index.htm で、対応するサーバー内の特定のパスは /home/qyn であり、検索されるファイルのパスになります。サーバーによっては /home /qyn/dxf/test/index.htm になるはずです。

クエリ文字列:
uid=1 はリクエスト中に提供される追加パラメータを意味します。これらのパラメータは KV キーと値のペアの形式で存在し、各クエリ文字列は & 記号で区切られます。

たとえば、Baidu で HTTP を検索すると、URL に多くのパラメータがあることがわかります。その多くのパラメータの中にパラメータ wd (単語) があり、これは検索時の検索キーワード wd=HTTP を意味します。

したがって、ネットワーク通信中に、双方が URL を介してユーザー データを送信できます。 

フラグメント識別子:

ch1 は、リソースの部分的な補足であるフラグメント識別子を表します。

グループ画像を見ると、フラグメント識別子が URL に表示されます。

1.3. urlencode と urldecode

/ ? : のような文字は、URL によって特別な意味として理解されます。したがって、これらの文字はランダムに出現することはできません。

たとえば、パラメータでこれらの特殊文字が必要な場合は、最初に特殊文字をエスケープする必要があります。

エスケープのルールは次のとおりです。

トランスコードする文字(文字は本質的には整数)を16進数に変換し、右から左に4桁(4桁未満は直接処理)を取り、2桁ごとに1桁にし、前に%を追加します、% XY 形式としてエンコードします。

下図のように、「+」は「%2B」にエスケープされます。

ノート:

1. 次の図に示すように、特殊文字に加えて、中国語の文字もエンコードおよびエスケープされます。

2. クライアントはリクエストを送信する前に urlencode のエンコード処理を実行する必要があり、サーバーはメッセージの受信後に urldecode デコード処理を実行する必要があります。エンコード (デコード) 処理は、シリアル化 (デシリアライズ) の前またはシリアル化 (デシリアル化) の後に行うことができます。 , 一般に、シリアル化後 (逆シリアル化の前) に urlencode エンコード (urldecode デコード) を実行することをお勧めします。

3. 特殊記号や漢字のエンコード結果を知りたい場合は、次の図に示すように、URL オンライン エンコード デコーダを検索できます。

4.urldecodeはurlencodeの逆の処理です。

1.4.HTTPプロトコル形式

アプリケーション層の共通プロトコルはHTTPとHTTPS、トランスポート層の共通プロトコルはTCP、ネットワーク層の共通プロトコルはIP、データリンク層はMACフレームに相当します。このうち、下位 3 つの層はオペレーティング システムまたはドライバーによって完成され、主に通信の詳細を担当します。アプリケーション層が次の 3 つの層を考慮しない場合、アプリケーション層自身の心の中では、相手のアプリケーション層と直接対話していると考えることができます。

下位 3 つの層は通信の詳細を担当し、アプリケーション層は送信されたデータの使用方法を担当します。2 つのホストが通信する場合、アプリケーション層のデータはピアのアプリケーション層に正常に配信されます。ネットワーク プロトコル スタック 3 番目の層は、このような通信の詳細を完了する役割を担っており、送信されたデータを使用するにはプロトコルをカスタマイズする必要があります。ここで最も一般的なのは HTTP プロトコルです。

HTTP は、リクエストとレスポンスに基づくアプリケーション層サービスです。クライアントとして、サーバーへのリクエストを開始できます。リクエストを受信した後、サーバーはリクエストのデータを分析して、アクセスしたいリソースを見つけます。次にサーバー応答を構築し、この HTTP 要求を完了します。リクエストとレスポンスに基づくこの動作方法は cs または bs モードと呼ばれます。c はクライアント、s はサーバー、b はブラウザを意味します。

HTTP はリクエストとレスポンスに基づくアプリケーション層のアクセスであるため、HTTP に対応するリクエスト形式と応答形式を知る必要があり、これが HTTP 学習の焦点です。

1.4.1.HTTPリクエストプロトコルの形式

HTTP リクエスト プロトコルの形式は次のとおりです。

HTTP リクエストは次の 4 つの部分で構成されます。

\cdot リクエスト行: [リクエスト メソッド]+[url] (通常はドメイン名とポートを削除し、パス URL のみを含めます)+[http バージョン] (クライアントがリクエストした http バージョン) リクエスト ヘッダー: リクエストの属性。属性は
\cdot キー付きです: 値の形式が行に表示されます (キー: キー内の値と値はコロン + スペースで区切られます)。
\cdot 空行: 空行がある場合は、リクエスト ヘッダーの終わりを示します。
\cdot リクエスト ボディ: リクエスト ボディは空の文字列にすることができます。リクエスト ボディが存在する場合は、リクエスト ボディの長さを識別するための Content-Length 属性がリクエスト ヘッダーに含まれます。
このうち、最初の 3 つの部分は通常 HTTP プロトコルに含まれており、HTTP プロトコル自体によって設定され、リクエストボディは通常、ユーザーの関連情報またはデータです。ユーザーがサーバーにアップロードする情報がない場合、リクエストの場合、リクエスト本文は空の文字列です。

注: リクエスト行の後にはリクエスト ヘッダーから区切るための \r\n があり、リクエスト ヘッダーの各 key:alue 属性の後にも \r\n があり、追加の \r\n がリクエストの後に追加されます。ヘッダーをリクエスト本文から分離します。

質問: HTTP リクエストのヘッダーとペイロードをどのように分離しますか?
回答:アプリケーション層が HTTP リクエストを受信すると、ペイロードから HTTP ヘッダーを分離する方法を見つける必要があります。HTTP リクエストの場合、ここでのリクエスト行とリクエスト ヘッダーは HTTP ヘッダー情報であり、ここでのリクエスト本文は実際には HTTP ペイロードです。

HTTP リクエストの空白行に応じて\r\n 区切ることができます。サーバーは HTTP リクエストを受信すると、それを 1 行ずつ読み取ることができます。2 つの空白行\r\n\r\n が読み取られた場合は、それを意味します。ヘッダーは読み取られており、実際の HTTP リクエスト内の 2 つの空白行 \r\n\r\n はヘッダーとペイロードを区切るために使用されます。

HTTP リクエストが大きな線形構造として想像される場合、各行の内容は \n で区切られるため、読み取りプロセス中に 2 つの \r\n が連続して読み取られる場合、ヘッダーが読み取られた後、残りはペイロードです。

1.4.2.HTTP応答プロトコルの形式

HTTP 応答プロトコルの形式は次のとおりです。

HTTP 応答は次の 4 つの部分で構成されます。

\cdot ステータス行: [http バージョン] (サーバーの対応する http バージョン) + [ステータス コード] + [ステータス コードの説明]
\cdot レスポンス ヘッダー: レスポンスの属性。これらの属性はキー: 値の形式で行ごとに表示されます。
\cdot 空行: 空行がある場合は、応答ヘッダーの終わりを示します。
\cdot 応答本文: 応答本文は空の文字列にすることができます。応答本文が存在する場合、応答本文の長さを識別するための Content-Length 属性が応答ヘッダーに存在します。たとえば、サーバーが HTML ページを返す場合、HTML ページのコンテンツは応答本文に含まれます。

ノート:

1. 一般的な 404 は、実際には、要求されたリソースには存在しないステータス コードです。

2. HTTP レスポンスのヘッダーとペイロードは、HTTP リクエストと同じ方法で分離されます。

質問: HTTP は、リクエストおよび応答メッセージのヘッダー部分とペイロード部分がすべて読み取られることをどのように保証しますか?

答え:

\cdot 完全なヘッダーを読み取る: 空白行が読み取られるまで 1 行ずつ読み取ります。

\cdot 完全なペイロードを読み取る: ヘッダーが読み取られた後、ヘッダーのリクエスト ヘッダーとレスポンス ヘッダーにはペイロード長が含まれており、ペイロード長に応じて読み取ることでペイロードを完全に読み取ることができます。

1.4.3. HTTP リクエストプロトコル形式の確認

以下の図 1 に示すように、server.hpp ファイルを作成し、以下の図 2 に示すように、serverTcp.cc ファイルを作成し、以下の図 3 に示すように Makefile ファイルを作成します。以下の図 4 に示すように、make コマンドを使用してserverTcp 実行可能プログラムを生成し、./udpServer 8081 コマンドを使用してserverTcp 実行可能プログラムを実行し、ブラウザを開いて「123.60.25.237:8081」と入力し、サーバーへのリクエストを開始します。 。

ここでは、URL としてサーバー アドレスとサーバー ポート番号のみを記述します。プロトコル スキーム名 http は、記述されていない場合はデフォルトで http プロトコルになるため、記述しません。ログイン情報、クエリ文字列、およびフラグメント識別子は記述できません。書き込まれますが、レイヤーを含むファイル パスは書き込めません。書き込みはデフォルトでサーバー固有のパスに行われます。

ここでのクライアントはブラウザであり、ブラウザはマルチスレッドであり、ブラウザが短期間に応答を取得できない場合、http リクエストを繰り返し送信するため、ここではサーバーが複数のリクエストを受信して​​出力します。

以下の図2ではリクエストライン、リクエストヘッダ、空行に相当する部分をマークしていますが、リクエストボディがないためここではマークしていません。

 

サーバー.hpp ファイル:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

using namespace std;

void handlerHttpRequest(int sock)
{
    cout << "+++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
   close(sock);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc ファイル:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

メイクファイル:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

注: 上記の図 2 のサーバーが受信するメッセージのリクエスト ヘッダー部分において、User-Agent はクライアントの属性情報、Accept はクライアントが受信できるリソースです。

1.4.4. HTTP 応答プロトコル形式の確認

以下の図に示すように、telnet www.baidu.com 80 コマンドを使用します。^] が表示された場合は、ログインが成功したことを意味します。ctrl+] を入力して Telnet コマンド ラインに入り、Enter キーを押し、GET / HTTP を使用します。 /1.1 コマンド (http リクエストの場合、コマンド文字のサイズは無視されます) (GET の後のフィールドはリクエストのリソース パスを示します。ここでは / のみを記述します) で、Baidu サーバーからの応答メッセージを取得します。

注: 上図のクライアントが受信したメッセージの応答ヘッダー部分の Content-Length は、対応する本文部分の長さです。

1.5. サーバー側の HTTP 応答コードの実装

以下の図 1 に示すように、server.hpp ファイルを作成し、以下の図 2 に示すように、serverTcp.cc ファイルを作成し、以下の図 3 に示すように Makefile ファイルを作成します。このパスの下に wwwroot フォルダーを作成し、wwwroot フォルダー内にindex.html ファイルを作成し、次の図 4 に示すコードを記述します。ファイルの関係は、次の図 5 に示します。以下の図 6 に示すように、make コマンドを使用してserverTcp 実行可能プログラムを生成し、./udpServer 8081 コマンドを使用してserverTcp 実行可能プログラムを実行し、ブラウザを開いて「123.60.25.237:8081」と入力し、サーバーへのリクエストを開始します。 Telnet 127.0.0.1 8081 コマンドを使用します。 ^] が表示された場合は、ログインが成功したことを意味します。ctrl+] を入力して Telnet コマンド ラインに入り、Enter キーを押します。GET / http/1.0 (大文字と小文字の両方) を使用します。 ) コマンドを使用します (以下の図 7 を参照)。

サーバー.hpp ファイル:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
    std::string path = getPath(buffer);

    std::string recource = ROOT_PATH;
    recource += path;
    std::cout << recource << std::endl;

    std::string html = readFile(recource);

    //开始响应
    std::string response;
    response = "HTTP/1.0 200 OK\r\n";
    response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc ファイル:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

メイクファイル:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

wwwroot フォルダー内のindex.html ファイル:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
</body>
</html>

ノート:

1. メッセージを送信してソケットにコンテンツを書き込むには、書き込みインターフェースまたは送信インターフェースを使用できます。送信インターフェース宣言は次の図に示されています。

パラメータ sockfd は書き込まれるソケット、パラメータ buf は送信されるデータ、パラメータ len は送信されるデータのサイズ、flags は 0 に設定され、書き込み関数は同じです。送信インターフェイスは、実際に書き込まれたバイト数を返します。

 send 関数を使用するには、<sys/types.h><sys/socket.h> ヘッダー ファイルをインクルードする必要があります。

2. サーバー応答メッセージの応答本文で、コンテンツの先頭と末尾に <html> を追加し、見出しとしてコンテンツの前後に <h1> を追加して、コンテンツのフォントを大きくすることができます。

応答本文に <html> と <h1> を追加すると、主流のブラウザはアプリケーションを認識できますが、応答ヘッダーで応答本文のタイプとその他の属性を指定することをお勧めします (例: "Content-type:text/html")説明レスポンスボディの型はhtmlテキスト型です。

3. 通常、サーバー応答メッセージの応答本文の内容はファイルから読み取られ、サーバーはクライアントが要求するファイル パスに従って対応するリソースを取得して応答するため、ここではファイル インデックスを作成します。 html、およびそのファイル内の応答本文のコンテンツ。

クライアントがリクエストを送信するとき、URL 内の階層ファイル パスがアクセスされるファイル パスになり、Telnet リクエスト行の 2 番目のフィールドがアクセスされるファイル パスになります。

ここでは、wwwroot フォルダーを作成し、応答本文のコンテンツが含まれるindex.html ファイルを wwwroot フォルダーに保存します。前にも述べたように、URL では、通常、階層ファイル パスの最初の / はルート ディレクトリではありません。ここでは、サーバーの特定のファイル パスを wwwroot として設定し、getPath を通じて URL 内の階層ファイル パスを取得します。インターフェイスに URL を変換し、中間の階層ファイル パスは特定のファイル パスの後に結合され、完全なファイル パスを取得します。完全なファイル パスに従って、サーバーは対応するファイル リソースの応答を読み取り、サーバーに送信します。クライアント。

実際、各 Web サイトで、クライアントから送信されたリクエストの URL 階層を含むファイル パスが単なる / である場合、サーバーは Web サイトの .html ホームページ リソースを使用してクライアントに応答します。インデックスをアップロードし、クライアントに返します。

4. ここでは、サーバー応答メッセージの応答本文部分をindex.html ファイルに保存します。index.html ファイルの内容はフロントエンド コードです。最初の行の <!DOCTYPE html> タグは、使用するドキュメントのブラウザーの HTML 仕様。<html> と </html> は開始タグと終了タグです。<meta charset="utf-8"> タグはエンコードの種類を示します。<title の間にタイトルの内容を入力します。 > タグと </title> タグ、< h1> と <h1> の間にタイトルの内容を入力します (h の後の数字はタイトルのサイズを示します。数字が大きいほど、タイトルは小さくなります。h1 を h3 に変更します)ここ)、<p></p> の間に段落の内容を記入します。

1.6. HTTPメソッド

HTTP の一般的なメソッドは次のとおりです。

最も一般的に使用されるのは、GET メソッドと POST メソッドです。

1.6.1. クライアントアップロードデータのコード例

クライアントアップロードデータのコード例:

server.hpp ファイル、serverTcp.cc ファイル、および Makefile は、セクション 1.5 のサーバー HTTP 応答コードに対応するものと同じです。次の図 1 に示すように、wwwroot フォルダー内の Index.html ファイルを変更します。以下の図 2 に示すように、make コマンドを使用してserverTcp 実行可能プログラムを生成し、./udpServer 8081 コマンドを使用してserverTcp 実行可能プログラムを実行し、ブラウザを開いて「123.60.25.237:8081」と入力し、サーバーへのリクエストを開始します。 。

wwwroot フォルダー内のindex.html ファイル:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>104 期测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

ノート:

1.index.html ファイルに新しい form タグを追加します。action はフォームの送信後に応答する必要がある Web ページを示します。ここでは他に Web ページがないため、最初にランダムな文字列を渡します。method は http メソッドを示します。 、br ラベルは改行を示し、<form...> と </form> の間はフォームの内容、input タグはブラウザの入力ボックスに対応し、type は入力ボックスの入力タイプを示し、text は入力ボックスの入力タイプを示します。テキストの種類、password はパスワードの種類、name は入力ボックスの名前を示します。

クライアント Web ページにユーザー パスワードを入力し、送信プロンプトをクリックすると、インデックス内のアクション部分が KV キーと値のペアとして、URL のクエリ文字列 KV キーと値のペアに対応します。下の図。

http では、get メソッドはユーザーに対応するパラメータ情報を平文の URL に結合します。

2. httpメソッドをpostに変更した場合、index.body部分のアクション部分のみとなります。

http では、post メソッドはユーザーに対応するパラメータ情報をプレーン テキストのリクエスト本文に結合して送信します。

1.6.2. GETメソッドとPOSTメソッド

GET メソッドは通常、特定のリソース情報を取得するために使用され、POST メソッドは通常、データをサーバーにアップロードするために使用されます。しかし実際には、データをアップロードするときに GET メソッドを使用することもあります。たとえば、Baidu はデータを送信するときに実際に GET メソッドを使用します。

GET メソッドと POST メソッドは両方ともパラメータを取ることができます。

\cdot GET メソッドは、URL を介してパラメータを渡します。 

\cdot POST メソッドは、本体を介してパラメーターを渡します。
GET メソッドと POST メソッドのパラメーター受け渡し形式から、URL の長さは制限されているため、POST メソッドはより多くのパラメーターを渡すことができ、POST メソッドは本文でパラメーターを渡すことでより多くのデータを運ぶことができることがわかります。 。 

さらに、POST メソッドを使用してパラメータを渡すと、POST メソッドはパラメータを URL にエコーせず、他の人に簡単に見られないため、よりプライベートになります。POST メソッドと GET メソッドは実際には安全ではなく、どちらもパラメータを平文で渡し、暗号化によってのみセキュリティを確保できるため、POST メソッドが GET メソッドより安全であるとは言えません。

1.7. HTTPステータスコード

1.7.1. HTTP ステータスコードの概要

HTTP ステータス コードは次のとおりです。

最も一般的なステータス コードには、200 (OK)、404 (見つかりません)、403 (禁止された要求の許可が十分ではありません)、302 (リダイレクト)、504 (不正なゲートウェイ) などがあります。

1.7.2.リダイレクトリダイレクトステータスコード

リダイレクトの概念:

リダイレクトとは、さまざまなネットワークリクエストをさまざまな方法で別の場所にリダイレクトすることであり、このときサーバーは案内サービスを提供することに相当します。

例: クライアントはサーバーへのリクエストを開始し、サーバーは応答するための応答を構築します。応答では、ステータス コードが 301/302 に設定され、応答ヘッダーには Location フィールド (後述) が含まれます。このフィールドの値は新しい URL アドレスで、クライアントに次にここにアクセスするように指示します。クライアントのブラウザは応答を受信すると、新しい URL にジャンプし、対応する新しいサーバーに要求を送信します (リダイレクト)。

リダイレクト (リダイレクトステータスコード):

リダイレクトは一時的なリダイレクトと永続的なリダイレクトに分けられ、ステータス コード 301 は永続的なリダイレクトを示し、ステータス コード 302 と 307 は一時的なリダイレクトを示します。

一時的なリダイレクトと永続的なリダイレクトの本質は、クライアントのラベルに影響を与え、クライアントがターゲット アドレスを更新する必要があるかどうかを判断することです。Web サイトが永続的にリダイレクトされる場合、初めて Web サイトにアクセスしたときにブラウザーがリダイレクトしますが、後で Web サイトにアクセスしたときにブラウザーがリダイレクトする必要はありません。リダイレクトされた Web サイト (クライアント ブラウザー) です。はリダイレクトされた URL を記憶します。クライアントがリダイレクト前に URL にアクセスすると、ブラウザーはデフォルトでリダイレクトされた URL に直接アクセスします)。また、Web サイトが一時的にリダイレクトされる場合、Web サイトにアクセスするたびにリダイレクトが必要な場合、ブラウザはリダイレクトの前にまずサーバーにリクエストを送信し、応答を受信した後にリダイレクトを完了する必要があります。応答に対応します。

注: 一時リダイレクトと永続リダイレクトの選択は、対応するサーバー リソースが一時的に利用できないか永続的に利用できないかによって異なります。更新などの理由で一時的に利用できない場合は、一時リダイレクトを使用します。リソース パスの置換により永続的に利用できない場合は、一時リダイレクトを使用します。など、永続的なリダイレクトを使用します。

1.7.3. リダイレクト リダイレクト ステータス コード コードの使用法

以下の図 1 に示すように、server.hpp ファイルを作成し、以下の図 2 に示すように、serverTcp.cc ファイルを作成し、以下の図 3 に示すように Makefile ファイルを作成します。以下の図 4 に示すように、make コマンドを使用してserverTcp 実行可能プログラムを生成し、./udpServer 8081 コマンドを使用してserverTcp 実行可能プログラムを実行し、ブラウザを開いて「123.60.25.237:8081」と入力し、サーバーへのリクエストを開始します。 Telnet 127.0.0.1 8081 コマンドを使用します。 ^] が表示されたら、ログインは成功しています。ctrl+] を入力して Telnet コマンド ラインに入り、Enter キーを押します。GET / http/1.0 (大文字と小文字の両方) を使用します。 ) コマンドを使用します (以下の図 5 を参照)。

サーバー.hpp ファイル:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;
    std::string response = "HTTP/1.1 302 Temporarily Moved\r\n";
    //std::string response = "HTTP/1.1 301 Permanently Moved\r\n";
    response += "Location: https://www.qq.com/\r\n"; 
    response += "\r\n";
    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc ファイル:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

メイクファイル:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

注: ステータス コード 302 は一時的なリダイレクトで、対応するステータスの説明は一時的に移動済み、ステータス コード 301 は永続的なリダイレクト、対応するステータスの説明は永続的に移動済みです。応答ヘッダーに Location キーワードを設定すると、対応する値がリダイレクトされる URL になります。

1.8.HTTP共通ヘッダリクエスト/レスポンスヘッダ

1.8.1. HTTP 共通ヘッダーのリクエスト/レスポンスヘッダーの概要

一般的な HTTP ヘッダーは次のとおりです。

ヘッダー リクエスト/レスポンスヘッダー 関数
コンテンツタイプ データ型(text/htmlなど)
コンテンツの長さ テキストの長さ
ホスト クライアントはサーバーに、要求されたリソースがどのホスト上のどのポートにあるかを伝えます。
ユーザーエージェント ユーザーのオペレーティング システムとブラウザのバージョン情報を宣言します
参照 現在のページはどのページからリダイレクトされていますか?
位置 3XX ステータス コードとともに使用して、クライアントに次に訪問する場所を伝えます。
繋がり リンク方式(ロングリンク/ショートリンク)
クッキー クライアント上に少量の情報を保存するために使用され、通常はセッション (セッション) の機能を実装するために使用されます。

ホスト:

\cdot Host フィールドは、クライアントがアクセスしたいサービスの IP とポートを示します。たとえば、ブラウザがサーバーにアクセスすると、ブラウザから送信される HTTP リクエストの Host フィールドには、IP とポートが入力されます。しかし、クライアントはサーバーにアクセスするだけではないでしょうか? なぜクライアントは、アクセスしたいサービスに対応する IP とポートをサーバーに伝える必要があるのでしょうか?

\cdot 一部のサーバーは実際にプロキシ サービスを提供するため、つまり、クライアントの代わりに他のサーバーへのリクエストを開始し、リクエストの結果をクライアントに返します。この場合、クライアントは、アクセスしたいサービスに対応する IP とポートをプロキシ サーバーに伝える必要があり、ホストから提供された情報が有効になります。

ユーザーエージェント:

\cdot User-Agentはクライアントに対応するOSやブラウザのバージョン情報を表します。
\cdot たとえば、コンピューターを使用して何らかのソフトウェアをダウンロードすると、オペレーティング システムに一致するバージョンが自動的に表示されます。これは、実際には、ターゲット Web サイトへのリクエストを開始するときに、ユーザー エージェント フィールドにホスト情報が含まれているためです。この時点で、Web サイトは一致するソフトウェア バージョンをプッシュします。

繋がり:

\cdot HTTP/1.0 は、リクエストとレスポンスのメソッドを通じてリクエストとレスポンスを行います。HTTP/1.0 の一般的な動作方法は、まずクライアントとサーバーが接続を確立し、次にクライアントがサーバーへのリクエストを開始し、サーバーがリクエストに応答するというものです。その後すぐにポートが接続されます。
\cdot しかし、接続確立後にクライアントとサーバーが一度しかやり取りしないと接続が閉じられてしまい、リソースが無駄になってしまうため、現在主流の HTTP/1.1 では長い接続がサポートされています。いわゆる長い接続とは、接続が確立された後、クライアントが一度に複数の HTTP リクエストをサーバーに連続的に書き込むことができ、サーバーがこれらのリクエストを上位層で順番に読み取ることができることを意味します。大量のリクエストとレスポンスがあり、接続が長くなります。
HTTP リクエストまたはレスポンス ヘッダーの Connect フィールドに対応する値が Keep-Alive である場合は、長い接続がサポートされていることを意味し、Connect フィールドに対応する値がクローズされている場合は、短い接続のみがサポートされていることを意味します。

ノート:

1. ユーザーが見る完全な Web ページ コンテンツは、無数の http リクエストの背後にある可能性があります。http の最下層の主流は tcp プロトコルです。Web ページの各リクエストが短いリンクを使用している場合、n 個の http リクエストの最下層には、ハンドシェイクと 4n 回の手を振ると、Web ページの取得速度が大幅に低下するため、短いリンクの実際の効率は低くなります。

2. http プロトコルは実際にはコネクションレス型です。http プロトコルと tcp プロトコルは異なる層のプロトコルであるため、2 つのプロトコルは互いに関係がありません。tcp プロトコルは接続指向であり、http プロトコルとは何の関係もありません。tcp プロトコルは、完了するためのチャネルを確立します。 http の動作中は接続が確立されません。

3. HTTP はロングコネクションで接続を確立した後、複数のリクエストを連続してサーバーに送信しますが、サーバーがクライアントの要求順に応答しないと問題が発生する可能性があるため、1 つの接続では、サービス クライアントは、接続中に要求した順序で応答します。

1.8.2.クッキー

HTTP は実際にはステートレス プロトコルであり、HTTP の各リクエスト/レスポンス間に関係はありませんが、ブラウザを使用するとそうではないことがわかります。

たとえば、CSDN に一度ログインした後、CSDN Web サイトを閉じたり、コンピュータを再起動したりしても、再度 CSDN Web サイトを開いたときに、CSDN はアカウント番号とパスワードの再入力を要求しません。これは実際に実現されています。 Cookie技術により、クリックするとブラウザの鍵マークから該当Webサイトの各種Cookieデータを確認することができます。

インターネットをサーフィンするとき、ユーザーはセッション保持を必要とします。セッション保持には多くの戦略があり、Cookie 戦略もその 1 つです。

これらの Cookie データは、実際には対応するサーバーによって書き込まれます。対応する Cookie の一部を削除した場合、削除した内容がログイン時に設定した Cookie である可能性があるため、この時点で再ログインと認証が必要になる場合があります。情報。

クッキーとは何ですか?

HTTPはステートレスなプロトコルであるため、Cookieがないとページリクエストをするたびに認証のためにアカウント番号とパスワードを再入力する必要があり、非常に面倒です。

たとえば、あなたが特定の動画 Web サイトの VIP である場合、この Web サイトには数百の VIP 動画があり、動画をクリックするたびに再度 VIP 認証を行う必要があります。ただし、HTTP はユーザー ステータスの記録をサポートしていないため、これをサポートするための独立したテクノロジが必要であり、現在このテクノロジは Cookie と呼ばれる HTTP プロトコルに組み込まれています。

ウェブサイトに初めてログインする際、本人認証のためアカウント番号とパスワードの入力が必要となりますが、この際、サーバーがデータ照合の結果、正当なユーザーであると判断した場合には、特定の Web ページ要求を後で行う アカウント番号とパスワードを時々再入力する必要はなく、この時点でサーバーが Set-Cookie を設定します。(Set-CookieキーワードもHTTPヘッダーの属性情報の一種です)

認証が成功し、サーバー側で Set-Cookie が設定されると、サーバーはブラウザーへの HTTP 応答時に Set-Cookie をブラウザーに応答します。ブラウザは応答を受信すると、Set-Cookie の値を自動的に抽出し、ブラウザの Cookie ファイルに保存します。このとき、ローカル ブラウザの Cookie ファイルに自分のアカウントとパスワードの情報を保存するのと同じことになります。 。

最初のログイン認証の後、ブラウザによって Web サイトに対して開始された HTTP リクエストには、最初の認証情報を伝える Cookie フィールドが自動的に含まれます。その後、ピア サーバーがユーザーを認証する必要がある場合、Cookie が直接抽出されます。アカウント番号とパスワードの再入力を求められることなく、HTTP リクエストのフィールドに入力できます。

つまり、最初の認証とログインの後、それ以降の認証はすべて自動認証になります。これを Cookie テクノロジーと呼びます。

以下の図 1 に示すように、server.hpp ファイルを作成します。以下の図 2 に示すように、serverTcp.cc ファイルを作成します。以下の図 3 に示すように、Makefile ファイルを作成します。次に、このパスに wwwroot フォルダーを作成し、 wwwroot フォルダーの .html ファイルにインデックスを追加するには、以下の図 4 に示すコードを記述します。make コマンドを使用して、serverTcp 実行可能プログラムを生成し、./udpServer 8080 コマンドを使用して、serverTcp 実行可能プログラムを実行し、ブラウザを開いて 123.60.25.237:8080 を入力し、サーバーへの要求を開始し、サーバーが応答し、クライアントがブラウザは、以下の図 5 に示すように、応答の Set -Cookie 値に従って応答して Cookie を設定します。ブラウザが Cookie を設定した後、今後クライアント ブラウザが対応するサーバーにリクエストを送信するとき、リクエスト ヘッダーは以下の図 6 に示すように、Cookie フィールドを保持します。

サーバー.hpp ファイル:

#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>

#define CRLF "\r\n"
#define SPACE " "
#define SPACE_LEN strlen(SPACE)
#define HOME_PAGE "index.html"
#define ROOT_PATH "wwwroot"

using namespace std;

std::string getPath(std::string http_request)
{
    std::size_t pos = http_request.find(CRLF);
    if(pos == std::string::npos) return "";
    std::string request_line = http_request.substr(0, pos);
    //GET /a/b/c http/1.1
    std::size_t first = request_line.find(SPACE);
    if(pos == std::string::npos) return "";
    std::size_t second = request_line.rfind(SPACE);
    if(pos == std::string::npos) return "";

    std::string path = request_line.substr(first+SPACE_LEN, second - (first+SPACE_LEN));
    if(path.size() == 1 && path[0] == '/') path += HOME_PAGE;
    return path;
}

std::string readFile(const std::string &recource)
{
    std::ifstream in(recource, std::ifstream::binary);
    if(!in.is_open()) return "404";
    std::string content;
    std::string line;
    while(std::getline(in, line)) content += line;
    in.close();
    return content;
}
void handlerHttpRequest(int sock)
{
    char buffer[10240];
    ssize_t s = read(sock, buffer, sizeof buffer);
    if(s > 0) cout << buffer;

    std::string path = getPath(buffer);

    std::string recource = ROOT_PATH;
    recource += path;
    std::cout << recource << std::endl;

    std::string html = readFile(recource);
    std::size_t pos = recource.rfind(".");
    std::string suffix = recource.substr(pos);
    cout << suffix << endl;

    //开始响应
    std::string response;
    response = "HTTP/1.0 200 OK\r\n";
    if(suffix == ".jpg") response += "Content-Type: image/jpeg\r\n";
    else response += "Content-Type: text/html\r\n";
    response += ("Content-Length: " + std::to_string(html.size()) + "\r\n");
    response += "Set-Cookie: this is my cookie content;\r\n";
    response += "\r\n";
    response += html;

    send(sock, response.c_str(), response.size(), 0);
}

class ServerTcp
{
public:
    ServerTcp(uint16_t port, const std::string &ip = "")
        : port_(port),
          ip_(ip),
          listenSock_(-1)
    {
        quit_ = false;
    }
    ~ServerTcp()
    {
        if (listenSock_ >= 0)
            close(listenSock_);
    }

public:
    void init()
    {
        // 1. 创建socket
        listenSock_ = socket(PF_INET, SOCK_STREAM, 0);
        if (listenSock_ < 0)
        {
            exit(1);
        }
        // 2. bind绑定
        // 2.1 填充服务器信息
        struct sockaddr_in local; // 用户栈
        memset(&local, 0, sizeof local);
        local.sin_family = PF_INET;
        local.sin_port = htons(port_);
        ip_.empty() ? (local.sin_addr.s_addr = INADDR_ANY) : (inet_aton(ip_.c_str(), &local.sin_addr));
        // 2.2 本地socket信息,写入sock_对应的内核区域
        if (bind(listenSock_, (const struct sockaddr *)&local, sizeof local) < 0)
        {
            exit(2);
        }

        // 3. 监听socket,为何要监听呢?tcp是面向连接的!
        if (listen(listenSock_, 5 /*后面再说*/) < 0)
        {
            exit(3);
        }
        // 运行别人来连接你了
    }
    void loop()
    {
        signal(SIGCHLD, SIG_IGN); // only Linux
        while (!quit_)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int serviceSock = accept(listenSock_, (struct sockaddr *)&peer, &len);
            if (quit_)
                break;
            if (serviceSock < 0)
            {
                // 获取链接失败
                cerr << "accept error ...." << endl;
                continue;
            }
            // 5.1 v1 版本 -- 多进程版本 -- 父进程打开的文件会被子进程继承吗?会的
            pid_t id = fork();
            assert(id != -1);
            if(id == 0)
            {
                close(listenSock_); //建议
                if(fork() > 0) exit(0);
                //孙子进程
                handlerHttpRequest(serviceSock);
                exit(0); // 进入僵尸
            }
            close(serviceSock);
            wait(nullptr);
        }
    }

    bool quitServer()
    {
        quit_ = true;
        return true;
    }

private:
    // sock
    int listenSock_;
    // port
    uint16_t port_;
    // ip
    std::string ip_;
    // 安全退出
    bool quit_;
};

serverTcp.cc ファイル:

#include "server.hpp"

static void Usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port" << std::endl;
    std::cerr << "example:\n\t" << proc << " 8080\n"
              << std::endl;
}

// ./ServerTcp local_port local_ip
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = atoi(argv[1]);

    ServerTcp svr(port);
    svr.init();
    svr.loop();
    return 0;
}

メイクファイル:

serverTcpd:serverTcp.cc
	g++ -o $@ $^ -std=c++11 -lpthread

.PHONY:clean
clean:
	rm -f serverTcpd

wwwroot フォルダー内のindex.html ファイル:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>104 期测试</title>
</head>
<body>
    <h3>hello my server!</h3>
    <p>我终于测试完了我的代码</p>
    <form action="/a/b/c.html" method="get">
        Username: <input type="text" name="user"><br>
        Password: <input type="password" name="passwd"><br>
        <input type="submit" value="Submit">
    </form>
</body>
</html>

1.8.3.セッション

メモリレベルとファイルレベル:

Cookie はブラウザ内の小さなファイルであり、ユーザーの個人情報を記録します。Cookie ファイルは 2 つのタイプに分類できます。1 つはメモリ レベルの Cookie ファイルで、もう 1 つはファイル レベルの Cookie ファイルです。

\cdot ブラウザを閉じて再度開き、以前にログインしたウェブサイトにアクセスします。アカウント番号とパスワードを再入力する必要がある場合は、以前にログインしたときにブラウザに保存されたCookie情報がメモリレベルにあることを意味します。 。
\cdot ブラウザを閉じるか、コンピュータを再起動して、以前にログインしたことのあるWebサイトにアクセスしてください。アカウントとパスワードを再入力する必要がない場合は、以前にログインしたときにブラウザに保存されているCookie情報が保存されていることを意味します。ファイルレベル。

盗まれたクッキー:

\cdot ブラウザに保存されているクッキー情報が不正利用者に盗まれると、不正利用者はあなたのクッキー情報を利用して、あなたが訪問したウェブサイトにあなたとしてアクセスできるようになり、この現象を「クッキーが盗まれた」と呼んでいます。
\cdot たとえば、誤って特定のリンクをクリックした場合、このリンクはダウンロード プログラムである可能性があります。クリックすると、何らかの方法でプログラムがローカル エリアにダウンロードされ、プログラムが自動的に実行されます。プログラムはブラウザをスキャンします。その中の cookie ディレクトリは、すべての cookie 情報をネットワーク経由で悪意のある当事者に送信します。悪意のある当事者があなたの cookie 情報を取得すると、その情報をブラウザに対応する cookie ディレクトリにコピーし、あなたと同じようにあなたの Web サイトにアクセスします。

セッションID:

\cdot このとき、Cookie ファイルには個人情報が保存されており、Cookie ファイルから個人情報が漏洩すると、個人情報も漏洩してしまうため、単純に Cookie を使用することは非常に危険です。

\cdot そこで、現在の主流のサーバーにもSessionIDという概念が導入されており、初めてWebサイトにログインしてアカウント番号とパスワードを入力すると、サーバー認証が成功した後にサーバー内にセッションファイルが作成され、このセッションにはユーザーの一時的な個人情報が格納されており、ファイル内では各ユーザーに対応するセッションファイル名がSessionIDであり、各ユーザーに対応するセッションファイル名、すなわちSessionIDは一意である。サーバーが生成するSessionIDはユーザー情報とは無関係で、システムはログインしている全ユーザーのSessionID値を統一して管理します。

\cdot このとき、サーバーは認証通過後にブラウザにHTTPレスポンスを返す際に、生成したSessionIDの値をブラウザに応答します。ブラウザが応答を受信すると、SessionID の値が自動的に抽出され、ブラウザの Cookie ファイルに保存されます。後でサーバーにアクセスすると、対応する HTTP リクエストに自動的に SessionID が送信されます。

\cdot サーバーは、HTTP リクエストに SessionID が含まれていることを認識すると、SessionID を抽出し、対応するコレクションと比較します。比較が成功した場合は、ユーザーが以前にログインしたことがあり、認証は自動的に成功します。そうすれば、送信したリクエストは通常​​どおり処理されますが、これが現在の主流の作業方法です。

セキュリティは相対的なものです。

SessionID が導入されると、ブラウザ内の Cookie ファイルに SessionID が保存されますが、このときに Cookie ファイルも盗まれる可能性があります。この際、ユーザーのアカウント番号とパスワードは漏洩しませんが、該当するユーザーのSessionIDが漏洩するため、不正なユーザーが私のSessionIDを盗み、私がアクセスしたサーバーにアクセスする可能性があり、先ほどの問題に相当します。

\cdot 以前の作業方法は、ブラウザに口座番号とパスワード情報を保存し、リクエストのたびに口座番号とパスワード情報を自動的に送信することに相当しますが、口座番号とパスワードを常にインターネット上に送信するのは安全ではありません。
\cdot したがって、現在の動作方法では、サーバーは初回認証時にアカウント番号とパスワードをネットワークに送信するだけで済み、その後、SessionID がネットワーク上に送信されます。
この方法は実際にはセキュリティ問題を解決するものではありませんが、比較的安全です。インターネットには絶対的なセキュリティという概念はなく、いかなるセキュリティも相対的なものであり、ネットワーク上に送信される情報を暗号化したとしても、他人に解読される可能性があります。

ただし、セキュリティの分野には法則があります。情報をクラッキングするコストが、クラッキング後に得られる利益よりもはるかに大きい場合 (これを実行すると金銭の損失になることを示します)、その情報は不正なものであると言えます。安全。

SessionID 導入後のメリット:

\cdot SessionID導入前は、ユーザーがログインしたアカウント情報はブラウザ内に保存されており、このときのアカウント情報はクライアント側で保持されていました。
\cdot SessionIDの導入後は、ユーザーがログインしたアカウント情報はサーバーで保持され、ブラウザ内にはSessionIDのみが保存されます。
このとき、SessionID は不正なユーザーによって盗まれる可能性がありますが、サーバーはユーザー アカウントのセキュリティを確保するためにさまざまな戦略を使用することもあります。

\cdot IPが分類されており、IPアドレスからログインユーザーのアドレス範囲を判断できます。アカウントのログイン アドレスが短期間に大幅に変更された場合、サーバーはアカウントが異常であることを即座に認識し、サーバー内の対応する SessionID 値をクリアします。このとき、あなたまたは不正なユーザーがサーバーにアクセスしようとする場合、本人認証のためにアカウント番号とパスワードを再入力する必要があり、パスワードを知っているのはあなただけです。再認証してログインすると、サーバーは相手を不正ユーザーと認識し、不正ユーザーに対してブラックリスト/ホワイトリスト認証を行います。

\cdot オペレーターが権限の高い操作を実行する場合、オペレーターは本人確認のためにアカウントとパスワードの情報を再入力する必要があります。たとえ不正利用者にアカウントを盗まれたとしても、パスワードを変更する際には古いパスワードを入力する必要がありますが、不正利用者はパスワードを知らないため、短時間で行うことはできません。このため、不正なユーザーはアカウントのパスワードを短時間で変更することができないため、盗難されたアカウントを取り戻すことができますが、このとき、現在の SessionID を回復することで無効にし、ユーザーが行ったアカウントを使用できるようになります。再ログイン認証を行います。

\cdot SessionID には有効期限ポリシーもあり、たとえば、SessionID は 1 時間以内に有効になります。そのため、たとえ不正なユーザーにSessionIDが盗まれても、有効期限は1時間以内であり、機能も制限されているため、大きな影響はありません。

何事にも裏表があり、このような不正利用者が存在しなければ、現在のサーバーは抜け穴だらけになっているはずであり、両者が対立し続けてこそ、双方が進歩し続けることができるのです。

おすすめ

転載: blog.csdn.net/qq_45113223/article/details/130778488