目次
ネットワークプログラミングは、プログラムがネットワーク通信を使用できるように実際にアプリケーションを作成することです
ここでは、トランスポート層によって提供される API を呼び出す必要があります。
トランスポート層によって提供されるプロトコルは 2 つあります。
1、UDP
2、TCP
これら 2 つのプロトコルは、2 つの異なる API セットを提供します
1. UDPとTCPの特徴の比較
UDP: コネクションレス、信頼性の低い伝送、データグラム指向、全二重
TCP: 接続、信頼性の高い伝送、バイト ストリーム指向、全二重
1. 接続ありと接続なし
以前に JDBC を学習していたとき、次のステップがありました。
まず DtaSourse を作成し、次に DataSourse を介して接続を作成します。
そして、ここでの接続は私たちが接続と呼ぶものです
より直感的な理解:
電話をかけるときは、番号をダイヤルしてダイヤルキーを押すと、相手がつながったときにのみ接続が完了します。
TCP をプログラミングする場合も、接続を確立する同様のプロセスがあります。
No connectionはWeChat/SMSの送信と同様で、接続を確立せずに通信を行うことができます。
ここでいう「つながり」とは抽象的な概念です
クライアントとサーバーの間では、相手側に関する情報を保存するためにメモリが使用されます。
双方がこの情報を保存すると、「接続」が表示されます。
1 つのクライアントから複数のサーバーに接続することができ、また 1 つのサーバーが複数のクライアントの接続に対応することもできます。
2. 確実な伝送と不安定な伝送
確実な送信とは、A から B に送信されたメッセージが 100% 送信できることを意味するものではありません。
代わりに、A は B にメッセージを送信するために最善を尽くします。送信が失敗した場合、A はそれを感知でき、送信が成功した場合、送信が成功したことを知ることもできます。
TCPは確実に伝送できるが、伝送効率が低い
UDPは伝送の信頼性が低いが、伝送効率が高い
TCP は信頼性の高い伝送、UDP は信頼性の低い伝送ですが、TCP が UDP よりも安全であるとは言えません。
「ネットワークセキュリティ」とは、送信するデータがハッカーによって簡単に傍受されないか、傍受された場合に重要な情報が漏洩するかどうかを指します。
3. バイトストリーム指向とデータグラム指向
TCP はファイル操作に似ており、どちらも「ストリーム」です (ここでの送信単位はバイトであるため、バイト ストリームと呼ばれます)。
UDP はデータグラム指向であり、読み取りと書き込みの基本単位は UDP データグラム (一連のデータ/属性を含む) です。
4. 全二重と半二重
全二重: 双方向に通信できる 1 つのチャネル
半二重: 一方向のみに通信できるチャネル
ネットワーク ケーブルは全二重です。
ネットワーク ケーブルには、4 本のグループで合計 8 本の銅ケーブルがあり、一部は一方向を担当し、一部は他の方向を担当します。
2. UDPソケット.api
1、データグラムソケット
ソケットオブジェクトです。
オペレーティング システムは、ファイルの概念を使用して、一部のソフトウェアおよびハードウェア リソースを管理します。また、オペレーティング システムは、ファイルを使用してネットワーク カードを管理します。ネットワーク カードを表すファイルは、ソケット ファイルと呼ばれます。
Java の Socket オブジェクトは、システムの Socket ファイルに対応します (最終的にはネットワーク カードに配置されます)。
ネットワーク通信には、Socket オブジェクトが必要です
施工方法:
最初のポートはクライアントでよく使用されます (クライアントが使用するポートはシステムによって自動的に割り当てられます)。
2 番目のポートはサーバー側でよく使用されます (サーバーが使用するポートは手動で指定されます)。
クライアントのホストには多くの実行結果があり、手動で選択したポートが他のプログラムによって占有されているかどうかは神のみぞ知るため、システムに自動的にポートを割り当てさせる方が賢明な選択です。
サーバーはプログラマーによって完全に制御され、プログラマーはサーバー上に複数のプログラムを配置し、それらに異なるポートを使用させることができます。
その他の方法:
2、データグラムパケット
UDP データグラムを表します。システムに設定されている UDP データグラムのバイナリ構造を表します。
施工方法:
最初のコンストラクター: データを受け入れるために使用されます
DatagramPacket は UDP データグラムとして、何らかのデータを伝送できる必要があります
データを保存するスペースとして手動で指定した byte[] を使用します
2 番目の構築方法: データの送信に使用されます
SocketAddress アドレスは IP とポート番号を指します
その他の方法:
getData は、UDP データグラム ペイロード部分 (完全なアプリケーション層データグラム) を取得することを指します。
エコーサーバーの実装
次に、UDP クライアント サーバーの手書きを開始します。
最も単純な UDP サーバー: エコー サーバー、クライアントが送信するもの、サーバーが返すもの
(1) サーバーコード
ネットワーク プログラムを作成する場合、ソケットが失敗する可能性があることを意味する、この種の例外が頻繁に発生します。
最も一般的な状況は、ポート番号が占有されている場合です。
ポート番号は、ホスト上のアプリケーションを区別するために使用されます。1 つのアプリケーションはホスト上の複数のポートを占有することができますが、1 つのポートは 1 つのプロセスによってのみ占有することができます (特殊なケースがあるため、ここでは説明しません)。
つまり、ポートがすでに他のプロセスによって占有されているときに、このソケット オブジェクトを作成してこのポートを占有しようとすると、エラーが報告されます。
サーバーは多くのクライアントにサービスを提供する必要がありますが、サーバーはクライアントがいつ来るかわかりません。サーバーは「常に準備ができていて」、クライアントが来たときにいつでもサービスを提供できることしかありません。
サーバーには、動作中に実行する 3 つの主要なタスクがあります。
1. リクエストを読み取り、解析します。
2. リクエストに基づいてレスポンスを計算します
。 3. レスポンスをクライアントに書き戻します。
エコーサーバーの場合、2番目のプロセスは気にせず、リクエストが何であれ、レスポンスを返します。
ただし、商用グレードのサーバーの場合、メイン コードは 2 番目のステップを完了しています。
このメソッドでは、パラメータの DatagramPacket が「出力パラメータ」になります。
レシーブに渡されるのは空のオブジェクトであり、レシーブ内には空のオブジェクトの内容が埋め込まれ、受信の実行が終了すると内容が埋め込まれたDatagramPacketが取得されます。
データを保存するためにこのオブジェクトが使用するメモリ空間は手動で指定する必要があります。これまでに学習したコレクション クラスとは異なり、独自の内部メモリ管理機能 (メモリの適用、メモリの解放、メモリ拡張などが可能) を備えています。
4096という数字は何気なく書かれていますが、あまり大きく書くことはできず、64kbを超えてはいけません。
サーバープログラムが開始されるとすぐにループが実行され、すぐに受信されます。
クライアントのリクエストがまだ到着していない場合、receive メソッドはブロックされ、クライアントが実際にリクエストを開始するまで待機します。
このコードでは、DatagramPacket オブジェクトが構築され、応答がクライアントに返されます。
注: 2 番目のパラメータを応答、length() として記述することはできません。
これは、response,length() が文字単位で長さを計算するのに対し、response.getBytes().length はバイト単位で長さを計算するためです。
文字列が英語の場合、バイト数と文字数は同じですが、文字列に漢字が含まれる場合、両者の計算結果は異なります。
ソケット API 自体はバイト単位で処理されます
requestPacket.getSocketAddress() この部分はクライアントにデータグラムを送信する部分であり、クライアントの IP とポートを知る必要があります。
DatagramPacket このオブジェクトには、通信相手の IP とポートが含まれます。
サーバー部品コード:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//UDP 的回显服务器
//客户端发的请求是啥,服务器的响应就是啥
public class UdpEchoServer {
private DatagramSocket socket = null;
public UdpEchoServer(int port) throws SocketException {
socket = new DatagramSocket(port);
}
//使用这个方法启动服务器
public void start() throws IOException {
System.out.println("服务器启动");
while (true){
//反复的,长期的执行针对客户端请求处理的逻辑
//一个服务器,运行过程中,要做的事情,主要是三个核心环节
//1、读取请求,并解析
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
socket.receive(requestPacket);
//这样的转字符串的前提是,后续客户端发的数据就是一个文本的字符串
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
//2、根据请求,计算出响应
String response = process(request);
//3、把响应写回给客户端
//此时需要告知网卡,要发的内容是啥,要发给谁
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//记录日志,方便观察程序执行效果
System.out.printf("[%s:%d] req: %s, resp: %s\n",responsePacket.getAddress().toString(),responsePacket.getPort(),
request,response);
}
}
//根据请求计算响应
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer server = new UdpEchoServer(9090);
server.start();
}
}
ここは閉店していますか?
サーバープログラムの場合、DatagramSocket が閉じていなくても、プログラム全体でソケット オブジェクトが 1 つだけ存在し、頻繁に作成されるわけではないため、大きな問題はありません。
このオブジェクトのライフサイクルは非常に長く、プログラム全体に従うため、現時点ではソケットを開いたままにしておく必要があります。
ソケット オブジェクトは、システム内のソケット ファイルとファイル記述子に対応します (主な目的は、ソケット オブジェクトを閉じて、ファイル記述子が使用可能かどうかを判断することです)。
プロセスが終了すると、PCB はリサイクルされ、内部のファイル記述子テーブルは破棄されます。
ただし、これは以下に限定されます。ソケット オブジェクトは 1 つだけであり、ライフサイクルはプロセスに従うため、現時点では解放する必要はありません。
ただし、複数のソケット オブジェクトがある場合、ソケット オブジェクトのライフ サイクルは短くなり、頻繁に作成および解放する必要があります。
(2) クライアントコード
クライアントを作成します。次のパラメーターは、送信先の IP とポートを指定します。
ここで必要なのは InetAdress オブジェクトなので、InetAdress の静的メソッド getByName を使用して構築します (ファクトリ モード/ファクトリ メソッド)
サーバーは起動時に自動的に 9090 にバインドされます。
クライアントは次にウィンドウ 9090 にアクセスします。
クライアントコード:
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket = null;
private String serverIp;
private int serverPort;
//服务器的 ip 和 服务器窗口
public UdpEchoClient(String ip ,int port) throws SocketException {
serverIp = ip;
serverPort = port;
//这个 new 操作,就不再指定端口了,让系统自动分配一个空闲端口
socket = new DatagramSocket();
}
//客户端启动,让这个客户端反复的从控制台读取用户输入的内容,把这个内容构造成 UPD 请求,发给服务器,再读取服务器返回的 UDP响应
//最终再显示在客户端的屏幕上
public void start() throws IOException {
System.out.println("客户端启动!");
Scanner scanner = new Scanner(System.in);
while (true){
//1、从控制台读取用户输入的内容
System.out.println("-> ");//命令提示符,提示用户输入字符串
String request = scanner.next();
//2、构造请求对象,并发给服务器
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
//3、读取服务器的响应,并解析出响应内容
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,responsePacket.getLength());
//4、显示结果
System.out.println(request);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.1",9090);
client.start();
}
}
この時点で、最初にサーバーを実行し、次にクライアントを実行し、コンテンツを入力して実行結果を確認します。
複数のクライアントが起動されている場合、複数のクライアントをサーバーで処理することもできます。
翻訳サーバーの導入
翻訳サーバーはいくつかの英語の単語を要求し、応答は対応する中国語の翻訳です。
このサーバーは前のエコー サーバーのコードの一部に似ているため、前のサーバーを直接継承させます。
継承自体は「既存のコードの再利用」を改善するためのものです。
ここで @Override を付けないと、メソッド名/パラメータの型/パラメータの数/アクセス権限を間違えると、その時点で書き換えができなくなり、探すのが難しくなります。
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;
public class UdpDicSever extends UdpEchoServer{
private Map<String,String> dict = new HashMap<>();
public UdpDicSever(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("duck","小鸭");
//可以在这里继续添加千千万万个单词,每个单词都有一个对应的翻译
}
//是要复用之前的代码,但是又要做出调整
@Override
public String process(String request){
//把请求对应单词的翻译给返回回去
return dict.getOrDefault(request,"该词没有查询到");
}
public static void main(String[] args) throws IOException {
UdpDicSever server = new UdpDicSever(9090);
//start 就不需要在写一遍了,就直接复用了之前的 start
server.start();
}
}
3.TCP
TCP はバイト ストリームであり、バイトごとに送信されます。
つまり、TCP データグラムはバイト配列 byte[] です。
TCPが提供するAPIにも2つのクラスがあります
1、サーバーソケット
サーバーが使用するソケット
施工方法:
その他の方法:
2、ソケット
これはサーバーとクライアントの両方で使用されます。
施工方法:
その他の方法:
エコーサーバー
(1) サーバーコード
それでは、TCP バージョンのエコー サーバーを作成してみましょう
UDP とはいくつかの違いがあります。
ループに入ると、クライアントのリクエストを読み取るのではなく、クライアントの「接続」を最初に処理する必要があります。
カーネルには多数の接続がありますが、アプリケーションではそれらを 1 つずつ処理する必要があります。
カーネル内の接続は To Do 項目のようなもので、これらの To Do 項目はキュー データ構造内にあり、アプリケーションはこれらのタスクを 1 つずつ完了する必要があります。
タスクを完了するには、まずタスクを取得し、serverSocker.accept() を使用する必要があります。
カーネル内の接続がアプリケーションに取得されるこのプロセスは、「プロデューサー/コンシューマー モデル」に似ています。
プログラムが開始されると、すぐに実行されて受け入れられます。
サーバーが accept を実行するとき、クライアントはまだ到着していない可能性があり、accpet はブロックします。
クライアントが正常に接続するまでブロックする
Accept は、カーネル内で確立された接続を取得し、それをアプリケーションに取得することです。
ただし、ここでの戻り値は「Connection」のようなオブジェクトではなく、単なる Socket オブジェクトです。
そしてこのSocketオブジェクトはヘッドセットのようなもので、通話したり相手の声を聞くことができます。
Socket オブジェクトを介してネットワーク上の相手と通信します。
TCP サーバーに関係するソケットには、serverSocket と clientSocket の 2 種類があります。
serverSocket は顧客を勧誘する営業部門の担当者、clientSocket は詳細を紹介する営業部門の担当者として理解できます。
IO 操作は比較的高価です
メモリへのアクセスに比べて、IO の実行回数が増えるほどプログラムの速度は遅くなります。
メモリの一部をバッファとして使用し、データを書き込むときは、最初にバッファに書き込み、データの波形を保存し、均一に IO を実行します。
PrintWriter にはバッファが組み込まれており、ここでのデータが実際にネットワーク カード経由で送信され、バッファに残らないように手動で更新できます。
ここでフラッシュを追加した方が安全ですが、追加しなくても問題が発生するわけではありません。
バッファには特定のリフレッシュ戦略が組み込まれているため、フラッシュを追加することをお勧めします。
このプログラムには 2 種類のソケットが関係します
1. ServerSocket (1 つだけあり、ライフサイクルはプログラムに従います。閉じていなくても問題ありません)
2、ソケット
ここのソケットは繰り返し作成されます
接続が切断された後にソケットを閉じることができることを確認する必要があるため、最後にソケットを閉じるコードを追加します。
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
private ServerSocket serverSocket = null;
//这个操作就会绑定端口号
public TcpEchoServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
System.out.println("启动服务器!");
while (true){
Socket clientSocket = serverSocket.accept();
processConnection(clientSocket);
}
}
//通过这个方法处理一个连接的逻辑
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s,%d] 客户端上线!\n,", clientSocket.getInetAddress().toString(),clientSocket.getPort());
//接下来就可以读取请求,根据请求计算响应,返回响应
//Socket 对象内部,包含两个字节流对象,可以把这两个字节流对象获取到,完成后续的读写工作
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()){
//一次连接中,可能会涉及到多次请求 / 响应
while (true){
//1、读取请求,并解析,为了读取方便,直接使用Scanner
Scanner scanner = new Scanner(inputStream);
if (!scanner.hasNext()){
//读取完毕,客户端下线
System.out.printf("[%s:%d] 客户端下线! \n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
break;
}
//这个代码暗含一个约定:客户端发过来的请求,得是文本数据,同时,还得带有空白符作为分割
String request = scanner.next();
//2、根据请求,计算响应
String response = process(request);
//3、把响应写回给客户端,把 OutputStream 使用 PrinterWriter 包裹一下,方便进行发数据
PrintWriter writer = new PrintWriter(outputStream);
//使用 PrintWriter 的 println 方法,把响应返回给客户端
//此处使用 println 而不是 print 就是为了在结尾加上换行符,方便客户端读取响应,使用 Scanner.next 读取
writer.println(response);
//这里还需要加一个 “刷新缓冲区” 的操作
writer.flush();
//日志,打印一下当前的请求详情
System.out.printf("[%s:%d] req: %s,resp: %s\n",clientSocket.getInetAddress().toString(),clientSocket.getPort(),
request,response);
}
}finally {
//在 finally 中,加上 close 操作,确保当前 socket 能够被及时关闭
clientSocket.close();
}
}
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server = new TcpEchoServer(9090);
server.start();
}
}
(2) クライアントコード
クライアントがしなければならないこと:
1. コンソールからユーザー入力を読み取る
2. 入力コンテンツをリクエストに構築し、サーバーに送信します。
3. サーバーからの応答を読み取ります
4. コンソールに応答を表示します
現在のコードではScannerとPrintWriterが閉じていないのですが、ファイル漏洩は起こりますか?
そうしません!!!
ストリーム オブジェクトに保持されるリソースには 2 つの部分があります。
1. メモリ (オブジェクトが破壊されると、メモリはリサイクルされます)、while ループが 1 サイクル完了すると、メモリは自然に破壊されます。
2. ファイル記述子
Scanner と printWriter はファイル記述子を保持しませんが、inputstream と outputstream への参照を保持しており、これら 2 つのオブジェクトは閉じられています。
正確に言うと、ソケットオブジェクトによって保持されているので、ソケットオブジェクトを閉じるだけです。
すべてのストリーム オブジェクトがファイル記述子を保持するわけではないため、ファイル記述子を保持するには、オペレーティング システムが提供する open メソッドを呼び出す必要があります。
hasNext は、クライアントがリクエストを送信しないときもブロックします。クライアントが実際にデータを送信するかクライアントが終了するまでブロックされ、hasNext は戻ります。
現在のコードにはまだ大きな問題があります。2つのクライアントを起動すると何が起こるでしょうか?
現象:
最初のクライアントが接続された後、2 番目のクライアントは正しく処理できません。
サーバーはクライアントがオンラインであることを認識できないため、クライアントからのリクエストを処理できません。
最初のクライアントが終了すると、2 番目のクライアントによって送信された以前のすべてのリクエストが応答されます。
クライアントが来ると、accept が戻り、processConnection に入ります。
ループはクライアントが終了するまでクライアントのリクエストを処理し、メソッドは最初の層に戻るまで終了しません。
問題の鍵は、クライアントのリクエストを処理する過程で、accept を 2 回呼び出すことができない、つまり 2 番目のクライアントが来ても処理できないことです。
ここで、クライアントによる processConnection の処理自体の実行には時間がかかります。これは、クライアントがいつ終了するか、クライアントが送信するリクエストの数がわからないためです。
このメソッドの実行中に accept も呼び出すことができればよいのですが、このときマルチスレッドを使用できます。
メインスレッドで集客を担当し、集客後は新規スレッドを作成し、クライアントからの各種リクエストを新規スレッドで処理させることも可能です。
上記の改善後は、サーバー リソースが十分である限り、複数のクライアントを持つことが可能になります。
実際、今書いたコードがこのように書かれていない場合、たとえば、各クライアントはリクエストを 1 つだけ送信し、送信後に切断する必要がある場合、上記の状況はある程度緩和されますが、依然として同様の状況が発生します。問題。
複数のメッセージを処理すると、当然 processConnection の実行時間が長くなり、この問題はさらに深刻になります。
TCP プログラムを作成する場合、次の 2 つの作成方法が必要になります。
1. 1つのコネクションで送信されるリクエストとレスポンスは1つだけ(ショートコネクション)
2. 1 つのリクエストで複数のリクエストとレスポンスを送信できます (長い接続)
今、私たちはいます、つながりがあり、新しいスレッドがあります
頻繁に接続/切断を行うクライアントが多数ある場合、サーバーはスレッドの作成/解放を頻繁に行うことになります。
スレッド プールを使用する方が良い解決策です
この書き方は間違っています!!!
processConnection とメイン スレッドは別のスレッドです。processConnecton の実行中に、メイン スレッドの試行が完了します。これにより、clientSocket が使い果たされる前に閉じられます。
したがって、clientSocket を閉じるために processConnection に引き渡す必要があるため、次のように記述する必要があります。
スレッド プールを使用すると、スレッドの頻繁な作成と破棄が回避されますが、結局のところ、各クライアントは 1 つのスレッドに対応します。
サーバーに対応するクライアントが多数ある場合、サーバーは多数のスレッドを作成する必要があり、サーバーに多大なオーバーヘッドが発生します。
さらにクライアント数やスレッド数が増加すると、システムの負荷はますます重くなり、応答速度が大幅に低下します。
1 つのスレッド (または多くても 3 つまたは 4 つのスレッド) を使用して、多くのクライアントからの同時リクエストを効率的に処理する方法はありますか?
これはC10M 問題とも呼ばれます。同時に、1kw の同時クライアント要求があります。
多くの技術的手段が導入されていますが、そのうちの 1 つは非常に効果的/必要なものです: IO 多重化/IO 多重化
これは、高同時実行性 (C10M) を解決するための重要な手段の 1 つです
高い同時実行性の問題を解決するには、率直に言って、次の 4 つの言葉しかありません。
1. オープンソース: より多くのハードウェア リソースを導入する
2. スロットリング: ユニットのハードウェア リソースが処理できるリクエストの数を増やします。
IO 多重化はスロットリング手法であり、同じリクエストによるハードウェア リソースの消費が少なくなります。