简单的英译汉翻译程序
接着上篇博客,我们继续来学习 Socket 套接字的相关知识点,首先我们写一个 英译汉翻译程序
即客户端不变,把服务器代码进行调整,关键的逻辑就是把响应写回给客户端。
package network;
import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
//使用继承,是为了复用之前的代码 省下我们重复写代码的时间
public class UdpDictServer extends UdpEchoServer{
Map<String, String> dict = new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("pig","猪");
dict.put("cat","猫");
dict.put("dog","狗");
dict.put("owl","猫头鹰");
dict.put("giant panda","大熊猫");
}
@Override
public String process(String request){
return dict.getOrDefault(request,"该单词没有查到");
}
public static void main(String[] args) throws IOException {
UdpDictServer udpDictServer = new UdpDictServer(9090);
udpDictServer.start();
}
}
那么我们首先启动服务器。
然后启动我们的客户端,输入相应的英文单词,观察服务器的响应。
客户端这边没有问题,我们再点击到服务器这里观察一下。
到这里 UDP 版本的 客户端服务器代码 的全部内容了就已经讲完了,今天我们来学一下 TCP版本的客户端服务器代码。
TCP API
TcpEchoServer
SeverSocket(给 TCP服务器 用的)
Socket(既要给服务器用,又要给客户端用)。
我们先写 TCP版本 的 TcpEchoServer 的代码:
public class TcpEchoServer {
//serverSocket 只有一个,clientSocket 会给每个客户端都分配一个
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);
}
}
前面基本都和 UDP 的差不多,区别就是这里的 start 方法,由于 TCP 是有连接的,不是一上来就能读数据,需要先建立连接;accept 返回了一个socket对象,如果此时没有客户,那么这里 accept 就会阻塞。
接下来我们来写processConnection()方法的代码。
这里也分为三步:
step1: 循环处理每个请求,分别返回响应。
step2: 根据请求,计算响应。
step3:把这个响应返回给客户端。
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
//输入 从网卡读 输出 往网卡写
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//相当于字符流
PrintWriter printWriter = new PrintWriter(outputStream);//OutputStream字节流 PrintWriter字符流
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾了(对端关闭了)
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
//直接使用 scanner 读取一段字符串
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端 响应里也是要带上换行的
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req: %s;resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
观察上述代码,我们可以发现 clientSocket 执行了关闭操作,那么关闭是在干什么?释放资源。
释放资源的前提是这个资源不再使用了。对于UDP的程序 ServerSocket 来说,这些 socket 都是贯穿始终的,但是对于TCP来说,clientSocket 有很多,断开就不再需要了,每次都得保证处理完的连接都进行释放。
TcpEchoServer完整代码:
package network;
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 {
//serverSocket 只有一个,clientSocket 会给每个客户端都分配一个
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();
//clientSocket是服务器的Socket 通过这个Socket和客户端进行通信
processConnection(clientSocket);
}
}
//通过这个方法来处理一个连接
//读取请求
//根据请求计算响应
//把响应返回给客户端
private void processConnection(Socket clientSocket) throws IOException {
System.out.printf("[%s:%d] 客户端上线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
try(InputStream inputStream = clientSocket.getInputStream();
//输入 从网卡读 输出 往网卡写
OutputStream outputStream = clientSocket.getOutputStream()){
Scanner scanner = new Scanner(inputStream);//相当于字符流
PrintWriter printWriter = new PrintWriter(outputStream);//OutputStream字节流 PrintWriter字符流
while(true){
//1.读取请求
if(!scanner.hasNext()){
//读取的流到了结尾了(对端关闭了)
System.out.printf("[%s:%d] 客户端下线\n",clientSocket.getInetAddress().toString(),
clientSocket.getPort());
break;
}
//直接使用 scanner 读取一段字符串
String request = scanner.next();
//2.根据请求计算响应
String response = process(request);
//3.把响应写回给客户端 响应里也是要带上换行的
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s:%d] req: %s;resp: %s\n",clientSocket.getInetAddress(),
clientSocket.getPort(),request,response);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
clientSocket.close();
}
}
private String process(String request) {
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer tcpEchoServer = new TcpEchoServer(9090);
tcpEchoServer.start();
}
}
TcpEchoClinet
OK,接下来我们写 TcpEchoClinet 的代码。
public class TcpEchoClient {
private Socket socket = null;
public TcpEchoClient(String serverIp,int port) throws IOException {
//这个操作相当于让客户端和服务器建立 tcp 连接
//这里的连接连上了,服务器的 accept 就会返回
socket = new Socket(serverIp,port);
}
注意,这里传入的IP和端口号的含义表示和这个ip、端口号建立连接。
start()方法:
public void start(){
Scanner scanner = new Scanner(System.in);
try(InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream()){
PrintWriter printWriter = new PrintWriter(outputStream);
Scanner scannerFromSocket = new Scanner(inputStream);
while(true){
//1.从键盘上读取用户输入的内容
System.out.println("和服务器连接成功");
System.out.println("->");
String request = scanner.next();
//2.把读取的内容构造成请求,发送给服务器
//这里发送 是带有换行的
printWriter.println(request);
//把数据写入内存的缓冲区 等缓冲区满了 才会真正写网卡
//可以手动刷新缓冲区,让数据立即被写入网卡
printWriter.flush();
//3.从服务器读取响应内容
String response = scannerFromSocket.next();
//4.把响应结果显示到控制台上
System.out.printf("req: %s;resp: %s\n",request,response);
}
} catch (IOException e) {
e.printStackTrace();
}
}
main函数
public static void main(String[] args) throws IOException {
TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090);
client.start();
}
接下来先启动服务器
服务器启动成功,启动客户端,可以发现和服务器连接成功!
然后我们点到服务器这边观察一下,可以发现服务器已经和客户端建立连接,系统已经分配了端口号。
我们进行输入数据。
看服务器这边的响应。
那么我们结束客户端,看服务器这边的响应。
解决只能处理一个客户端的问题
虽然上述代码已经运行起来了,但是存在一个问题,就是当前的服务器同一时间只能处理一个连接,我们看如何验证?
如何在 idea 中启动多个客户端,此时我们需要配置一下,默认一个程序只能启动一个客户端
此时选中 Allow multiple instances 之后,就可以开启多个客户端。
我们启动两个客户端,看服务器这边有没有响应。
启动两个客户端后我们来进行观察
第一个客户端
第二个客户端
我们在第二个客户端进行输入,发现没有任何响应
此时观察服务器,发现只有一个客户端上线
我们发现服务器这边没有再次出现第二个客户端建立连接的结果,只有第一次的客户端程序可以得到响应。
那么这是为什么呢?
观察代码我们就会发现有一个客户端,就需要 accept 一次,有多个客户端,就需要 accept 多次。执行 accept 之后,就会进入 processConnection 。
processConnection 中也有一个循环,这个循环只有当前客户端退出了,循环才会结束。
循环结束,processConnection 方法才会结束,方法结束才会执行到第二次 accept。
那此时我们把第一个客户端关闭查看第二个客户端,我们可以看到第二个客户端立即连接成功
此时服务器已经和该客户端连接成功。
当然,我们的目的是希望同时能够给 客户端1 提供循环服务,又能够 循环的调用 accept。那我们如何解决呢?这里就得用到我们之前学的多线程啦!
解决方案:
让主线程循环调用accept,当有客户端连接上来的时候就让主线程创建一个新线程,由新线程负责客户端的若干个请求,这个时候多个线程看上去是同时执行的。
这里我们修改TcpEchoServer,在原有的TcpEchoServer基础上修改以下部分代码即可。
public void start() throws IOException {
System.out.println("服务器启动");
while(true){
Socket clientSocket = serverSocket.accept();
//如果直接调用,该方法会影响这个循环的二次执行,导致 accept 不及时了
//创建新线程,用新线程来调用 processConnection
//每次来一个新的客户端都创建一个新的线程
//clientSocket是服务器的Socket 通过这个Socket和客户端进行通信
Thread t = new Thread(()->{
try {
processConnection(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
});
t.start();
}
}
我们再次启动多线程版本的服务器代码,启动两个客户端,发现没有问题。
那么讲到这里我们的网络编程 Socket 就结束了,感谢小伙伴的支持!!