関連するブログ、ムークラス参照列この一連のJavaソースコードとシステムメーカーは簡潔Zhentiインタビュアー
この列の下には、GitHubのアドレスです:
ソースは解決:https://github.com/luanqiu/java8
記事デモ:HTTPS:// GitHubの。 com / luanqiu / java8_demo
クラスメートは必要に応じてそれを見ることができます)
Javaソースコードの分析とインタビューの質問-実践的な作業:ソケットとスレッドプールの使用の組み合わせ
ソケットインタビューの最後の質問は、一般的に、クライアントとサーバーの通信の簡単な例を書けるようにすることです。この記事では、このデモを一緒に作成します。
1要件
- Socket、ServiceSocket、およびその他のAPIを使用できます。
- クライアントとサーバー間のTCP通信の例を記述します。
- サーバー側の処理タスクには非同期処理が必要です。
- サーバーの処理能力は非常に弱いため、同時に処理できる要求は5つだけです。6番目の要求がサーバーに到達すると、サーバーは明確なエラーメッセージを返す必要があります:サーバーがビジー状態です。しばらくしてからもう一度お試しください。
要件は比較的単純です。唯一の複雑な点は4番目の点です。クライアントのリクエスト量を制御する必要があります。最初に、クライアントから送信されたリクエスト数を制御できないことを確認する必要があるため、サーバーからしか実行できません。サーバーからの電流を制限するなどの変換。
一部の学生は、ServerSocketのBacklogプロパティを使用して5に設定する必要があるとすぐに思うかもしれませんが、前の章で、バックログはクライアント接続数の制限を正確に表しておらず、サーバーも必要であると述べました特定のエラー情報を返します。バックログが有効であっても、カスタマイズされたエラーメッセージではなく、修正されたエラーメッセージのみを返します。
考えてみましょう。スレッドプールはこれを実行できるようです。スレッドプールのcoreSizeとmaxSizeを4に設定し、キューサイズを1に設定して、サーバーがリクエストを受信するたびにスレッドプールを判断するようにすることができます。キューにデータがない場合は、現在のサーバーが5番目の要求を処理しようとしています。現在の要求は6番目の要求であり、拒否する必要があります。
スレッドプールに参加するだけでも3番目のポイントを満たすことができ、サーバー側のタスクを非同期で実行できます。
2クライアントコード
クライアントコードは比較的単純で、サーバーからデータを要求するだけです。コードは次のとおりです。
public class SocketClient {
private static final Integer SIZE = 1024;
private static final ThreadPoolExecutor socketPoll = new ThreadPoolExecutor(50, 50,
365L, TimeUnit.DAYS, new LinkedBlockingQueue<>(400));
@Test
public void test() throws InterruptedException {
// 模拟客户端同时向服务端发送 6 条消息
for (int i = 0; i < 6; i++) {
socketPoll.submit(() -> {
send("localhost", 7007, "nihao");
});
}
Thread.sleep(1000000000);
}
/**
* 发送tcp
*
* @param domainName 域名
* @param port 端口
* @param content 发送内容
*/
public static String send(String domainName, int port, String content) {
log.info("客户端开始运行");
Socket socket = null;
OutputStream outputStream = null;
InputStreamReader isr = null;
BufferedReader br = null;
InputStream is = null;
StringBuffer response = null;
try {
if (StringUtils.isBlank(domainName)) {
return null;
}
// 无参构造器初始化 Socket,默认底层协议是 TCP
socket = new Socket();
socket.setReuseAddress(true);
// 客户端准备连接服务端,设置超时时间 10 秒
socket.connect(new InetSocketAddress(domainName, port), 10000);
log.info("TCPClient 成功和服务端建立连接");
// 准备发送消息给服务端
outputStream = socket.getOutputStream();
// 设置 UTF 编码,防止乱码
byte[] bytes = content.getBytes(Charset.forName("UTF-8"));
// 输出字节码
segmentWrite(bytes, outputStream);
// 关闭输出
socket.shutdownOutput();
log.info("TCPClient 发送内容为 {}",content);
// 等待服务端的返回
socket.setSoTimeout(50000);//50秒还没有得到数据,直接断开连接
// 得到服务端的返回流
is = socket.getInputStream();
isr = new InputStreamReader(is);
br = new BufferedReader(isr);
// 从流中读取返回值
response = segmentRead(br);
// 关闭输入流
socket.shutdownInput();
//关闭各种流和套接字
close(socket, outputStream, isr, br, is);
log.info("TCPClient 接受到服务端返回的内容为 {}",response);
return response.toString();
} catch (ConnectException e) {
log.error("TCPClient-send socket连接失败", e);
throw new RuntimeException("socket连接失败");
} catch (Exception e) {
log.error("TCPClient-send unkown errror", e);
throw new RuntimeException("socket连接失败");
} finally {
try {
close(socket, outputStream, isr, br, is);
} catch (Exception e) {
// do nothing
}
}
}
/**
* 关闭各种流
*
* @param socket
* @param outputStream
* @param isr
* @param br
* @param is
* @throws IOException
*/
public static void close(Socket socket, OutputStream outputStream, InputStreamReader isr,
BufferedReader br, InputStream is) throws IOException {
if (null != socket && !socket.isClosed()) {
try {
socket.shutdownOutput();
} catch (Exception e) {
}
try {
socket.shutdownInput();
} catch (Exception e) {
}
try {
socket.close();
} catch (Exception e) {
}
}
if (null != outputStream) {
outputStream.close();
}
if (null != br) {
br.close();
}
if (null != isr) {
isr.close();
}
if (null != is) {
is.close();
}
}
/**
* 分段读
*
* @param br
* @throws IOException
*/
public static StringBuffer segmentRead(BufferedReader br) throws IOException {
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
return sb;
}
/**
* 分段写
*
* @param bytes
* @param outputStream
* @throws IOException
*/
public static void segmentWrite(byte[] bytes, OutputStream outputStream) throws IOException {
int length = bytes.length;
int start, end = 0;
for (int i = 0; end != bytes.length; i++) {
start = i == 0 ? 0 : i * SIZE;
end = length > SIZE ? start + SIZE : bytes.length;
length -= SIZE;
outputStream.write(bytes, start, end - start);
outputStream.flush();
}
}
}
クライアントコードでは、主にクライアントの同時シミュレーションのためにスレッドプールを使用して、一度に6つの要求を送信します。サーバーが6番目の要求を処理しているときに予想されるように、特定のエラーメッセージがクライアントに返されます。
上記のコードの主なメソッドはsendメソッドで、主にサーバーから送信されたデータを処理し、サーバーの応答を処理します。
3サーバーコード
サーバーのロジックは2つの部分に分かれています。最初の部分はクライアント要求の数を制御することです。サーバーの機能を超えると、新しい要求は拒否されます。サーバーの機能が応答できる場合、新しい要求が配置されます。2番目の部分はサーバーです。タスクの実行ロジック。
3.1クライアント要求の制御
public class SocketServiceStart {
/**
* 服务端的线程池,两个作用
* 1:让服务端的任务可以异步执行
* 2:管理可同时处理的服务端的请求数
*/
private static final ThreadPoolExecutor collectPoll = new ThreadPoolExecutor(4, 4,
365L, TimeUnit.DAYS, new LinkedBlockingQueue<>(1));
@Test
public void test(){
start();
}
/**
* 启动服务端
*/
public static final void start() {
log.info("SocketServiceStart 服务端开始启动");
try {
// backlog serviceSocket处理阻塞时,客户端最大的可创建连接数,超过客户端连接不上
// 当线程池能力处理满了之后,我们希望尽量阻塞客户端的连接
// ServerSocket serverSocket = new ServerSocket(7007,1,null);
// 初始化服务端
ServerSocket serverSocket = new ServerSocket();
serverSocket.setReuseAddress(true);
// serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost().getHostAddress(), 80));
serverSocket.bind(new InetSocketAddress("localhost", 7007));
log.info("SocketServiceStart 服务端启动成功");
// 自旋,让客户端一直在取客户端的请求,如果客户端暂时没有请求,会一直阻塞
while (true) {
// 接受客户端的请求
Socket socket = serverSocket.accept();
// 如果队列中有数据了,说明服务端已经到了并发处理的极限了,此时需要返回客户端有意义的信息
if (collectPoll.getQueue().size() >= 1) {
log.info("SocketServiceStart 服务端处理能力到顶,需要控制客户端的请求");
//返回处理结果给客户端
rejectRequest(socket);
continue;
}
try {
// 异步处理客户端提交上来的任务
collectPoll.submit(new SocketService(socket));
} catch (Exception e) {
socket.close();
}
}
} catch (Exception e) {
log.error("SocketServiceStart - start error", e);
throw new RuntimeException(e);
} catch (Throwable e) {
log.error("SocketServiceStart - start error", e);
throw new RuntimeException(e);
}
}
// 返回特定的错误码给客户端
public static void rejectRequest(Socket socket) throws IOException {
OutputStream outputStream = null;
try{
outputStream = socket.getOutputStream();
byte[] bytes = "服务器太忙了,请稍后重试~".getBytes(Charset.forName("UTF-8"));
SocketClient.segmentWrite(bytes, outputStream);
socket.shutdownOutput();
}finally {
//关闭流
SocketClient.close(socket,outputStream,null,null,null);
}
}
}
collectPoll.getQueue()を使用します。サイズ()> = 1は、現在のサーバーが処理制限に達したかどうかを判断します。タスクがキューにキューイングされている場合、現在のサーバーが過負荷になっていることを意味します。新しいリクエストは拒否されました。キューにデータがない場合、サーバーは新しいリクエストを受け入れることもできます。
上記のコードのコメントは詳細なので、詳細には触れません。
3.2サーバータスクの処理ロジック
サーバーの処理ロジックは比較的単純です。主な手順は、クライアントのソケットから入力を読み取り、処理して、クライアントに応答を返すことです。
スレッドを使用して2秒間スリープし、サーバーの処理ロジックをシミュレートします。コードは次のとおりです。
public class SocketService implements Runnable {
private Socket socket;
public SocketService() {
}
public SocketService(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
log.info("SocketService 服务端任务开始执行");
OutputStream outputStream = null;
InputStream is = null;
InputStreamReader isr = null;
BufferedReader br = null;
try {
//接受消息
socket.setSoTimeout(10000);// 10秒还没有得到数据,直接断开连接
is = socket.getInputStream();
isr = new InputStreamReader(is,"UTF-8");
br = new BufferedReader(isr);
StringBuffer sb = SocketClient.segmentRead(br);
socket.shutdownInput();
log.info("SocketService accept info is {}", sb.toString());
//服务端处理 模拟服务端处理耗时
Thread.sleep(2000);
String response = sb.toString();
//返回处理结果给客户端
outputStream = socket.getOutputStream();
byte[] bytes = response.getBytes(Charset.forName("UTF-8"));
SocketClient.segmentWrite(bytes, outputStream);
socket.shutdownOutput();
//关闭流
SocketClient.close(socket,outputStream,isr,br,is);
log.info("SocketService 服务端任务执行完成");
} catch (IOException e) {
log.error("SocketService IOException", e);
} catch (Exception e) {
log.error("SocketService Exception", e);
} finally {
try {
SocketClient.close(socket,outputStream,isr,br,is);
} catch (IOException e) {
log.error("SocketService IOException", e);
}
}
}
}
4テスト
テスト時には、最初にサーバーを起動し、次にクライアントを起動し、最初にサーバーを起動し、次のようにログを出力します。
次に、クライアントを起動し、次のようにログを出力します。
最後にサーバー操作ログを確認します。
上記の操作からサーバーでは、要求のピーク時に5つの要求を同時に処理でき、残りの要求は正しいプロンプトで拒否できます。
5まとめ
そのため、コードはSocketClient、SocketServiceStart、SocketServiceに集中しており、開始シーケンスは最初にSocketServiceStartを開始してからSocketClientを実行することです。興味のある学生は自分でデバッグして印象を深めることができます。