Java 网络编程(二) --- TCP的socket的api
文章目录
- 网络编程
- 写一个简单的翻译服务器
- TCP的socket的api
- 两个关键的类
- 写一个TCP的回显服务器和客户端
- 服务器
- 客户端
网络编程
写一个简单的翻译服务器
-
基本的客户端服务器的过程:
1.读取请求并解析
2.根据请求计算响应
3.把响应写回到客户端 -
写一个简单的计算响应的逻辑:
基于 echo server 再写一个翻译服务器(带有一点点的业务逻辑)
请求是一个单词,响应就会返回对应的中文翻译
比如:
dog -> 小狗
cat -> 小猫
package network;import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;public class UdpDictServer extends UdpEchoServer{private HashMap<String,String> dict = new HashMap<>();public UdpDictServer(int port) throws SocketException {super(port);// 这里可以往表中插入几千几万个英文单词dict.put("cat","小猫");dict.put("dog","小狗");dict.put("pig","小猪");}// 重写process方法,再重写方法中完成翻译的过程// 翻译本质上就是 查表 的过程public String process(String request){return dict.getOrDefault(request,"该词再词典中不存在!");}public static void main(String[] args) throws IOException {UdpDictServer server = new UdpDictServer(9090);server.start();}
}
这里重写 process 方法,就可以再子类中组织你想要的 ‘‘业务逻辑’’ 了
多态的逻辑:
TCP的socket的api
- TCP的socket的api又和UDP的socket的api差异又很大,但是和文件操作有密切的联系
两个关键的类
- ServerSocket(给服务器用的类,使用这个类来绑定端口号)
- Socket(既会给服务器用又会给客户端用)
这两类都是用来表示socket文件的
(抽象了网卡这样的硬件设备) - 这里不需要有数据报的额外的类,因为TCP是字节流的,传输的基本单位是 byte
写一个TCP的回显服务器和客户端
服务器
- 对于TCP来说不需要手动指定对端的地址,前提是要先建立连接
- 对于服务器这边,主要是把建立好的连接从系统内核中拿到应用程序中
- 如果队列中有建立好的连接,就直接拿,如果没有就要等待,知道它有了,这和生产者消费者模型就很像
4. serverSocket 和 clientSocket 在这里是什么意思?都有什么作用呢?
5. 获取源ip和源端口,目的ip和目的端口的方法
6. 通过getInputStream()和getOutputStream()进行接收和发送(交互)
发送和接收以字节为单位进行展开的
7. TCP约定空白符为一个数据报
package network;import jdk.internal.util.xml.impl.Input;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;// 构造一个 ServerSocket 对象public TcpEchoServer(int port) throws IOException {serverSocket = new ServerSocket(port);}public void start() throws IOException {System.out.println("服务器启动!");while(true){// 通过 accept 方法把内核中已经建立好的连接拿到应用程序中// 建立连接的细节流程都是内核自动完成的,应用程序只需要 ‘’捡现成‘’ 的Socket clientSocket = serverSocket.accept();// 通过这个方法来处理当前的连接processConnection(clientSocket);}}// 通过这个方法来处理当前的连接private void processConnection(Socket clientSocket) {// 进入方法,打印一个日志,表示客户端有用户连上了System.out.printf("[%s:%d] 客户端上线!\n",clientSocket.getInetAddress(),clientSocket.getPort());// 接下来进行数据的交互try(InputStream inputStream = clientSocket.getInputStream();OutputStream outputStream = clientSocket.getOutputStream()){// 使用 try() 的方式,避免后续用完了流对象,忘记close// 由于客户端发来的数据可能是多条数据,对于多条数据就要循环处理while(true){Scanner scanner = new Scanner(inputStream);if(!scanner.hasNext()) {// 连接断开了,此时循环就要结束了System.out.printf("[%s:%d] 客户端下线!\n",clientSocket.getInetAddress(),clientSocket.getPort());break;}// 1. 读取请求并解析,此次以next作为读取请求的方式,next的规则是读到空白符就返回String request = scanner.next();// 2. 根据请求,计算响应String response = process(request);// 3. 把响应写回到客户端// 可以把 String 转成字节数组,写入到 OutputString// 也可以使用 PrintWriter 把 OutputStream 包裹一下,来写入字符串PrintWriter printWriter = new PrintWriter(outputStream);// 此处的 println 不是打印到控制台了,而是写入到 outputStream 对应的流对象中,也就是写入到clientSocket里面// 这个数据也是通过网络发送出去了(发给当前连接这个的另外一端)// 这里使用 println 带有\n是为了后续客户端这边可以使用 scanner.next 来读取数据printWriter.println(response);// 这里还要刷新缓冲区,如果没有刷新操作,后续数据可能仍然在内存中,没有被写入网卡printWriter.flush();// 4. 打印一下这次请求交互过程中内容System.out.printf("[%s:%d] req=%s, res=%s\n",clientSocket.getInetAddress(),clientSocket.getPort(),request,response);}} catch (IOException e) {e.printStackTrace();}}public String process(String request){// 这是回显服务器,请求和响应都是一样的return request;}
}
上面的这个服务器程序,现在还存在两个问题?
- 存在文件资源泄露的情况
2. try () 这里的没有关闭完全
解决方法:
把close放在代码的其他地方可以吗?
客户端代码写完才能解释第二个问题
客户端
- 如果启动多个客户端,多个客户端同时和服务器建立连接
如何启动多个客户端?
默认情况下,IDEA只允许一个代码只能创建一个进程,通过上述操作,勾选了 Allow multiple instances,此时就可以运行多个进程了
2. 出现的第二个问题:
启动服务器,并且启动第一个客户端,第一个客户端可以正常的工作,但是启动第二个客户端,并不能正常工作(输入数据不会回显)
为什么会出现上述问题呢?
是代码结构的问题,每次执行建立连接后,要么阻塞在第二个循环的 hasNext 中,要么第二个客户端要等待第一个客户端执行完它的请求和响应并回显,不能在第二个客户端运行时,及时地执行第二个客户端,没有回到第一个循环的accept中
解决方案:
可以把代码改成多线程的结构,让第一个循环中的processConnection方法变成多线程的,让主线程执行的同时(处理多个客户端的accept),副线程(当前执行的客户端)执行它的逻辑
- 总结TCP和UDP会不会产生这样的问题?
4. 上述代码还是有点问题,如果有多个客户端调用,频繁地建立连接和断开连接,就会频繁的创建线程和销毁线程,会产生大量的开销
5. 但是线程池也只能提升一点,下面还有解决办法
package network;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;public class TcpEchoClient {private Socket socket = null;public TcpEchoClient(String serverIp,int serverPort) throws IOException {// 这样写不行,还需要建立连接// 需要在创建Socket的同时,和服务器 建立连接 ,此时就告诉Socket服务器在哪里// 系统内核会自动建立连接,不需要我们进行代码干预socket = new Socket(serverIp,serverPort);// 当我们 new 对象的时候,操作系统内核,就开始 三次握手 的具体细节了,完成建立连接的过程了}public void start(){// TCP 和 UDP 的两者的客户端行为是差不多的// 过程:// 3. 从服务器读取响应// 4. 把响应回显到控制台上Scanner scanner = new Scanner(System.in);try(InputStream inputStream = socket.getInputStream();OutputStream outputStream = socket.getOutputStream()) {PrintWriter printWriter = new PrintWriter(outputStream);Scanner scannerNetwork = new Scanner(inputStream);while (true) {// // 1. 从控制台读取用户输入的数据System.out.println("-> ");String request = scanner.next();// 2. 把字符串作为请求发送给服务器// 这里使用 println 是为了让请求后面带上换行// 也就是和服务器读取请求, scanner.next 呼应printWriter.println(request);printWriter.flush();// 3. 读取服务器返回的响应String response = scannerNetwork.next();// 4.在界面上显示内容System.out.println(response);}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {TcpEchoClient server = new TcpEchoClient("127.0.0.1",9090);server.start();}
}