文章目录
TCP流套接字编程
ServerSocket类和Socket类这两个类都是用来表示socket文件(抽象了网卡这样的硬件设备)。
TCP是面向字节流的,传输的基本单位是byte 字节。和UDP不同,UDP传输的单位是数据报。
1.ServerSocket类
给服务器使用的类,用这个类来绑定端口号
2.Socket类
既会给服务器用,又会给客户端用
因为TCP的有连接的,会保存对端的连接。不用像UDP那样每次发送都要手动在send方法中指定目标地址。
TCP的建立连接,由系统内核自动负责完成的。客户端,要发起“建立连接”的动作。服务器,要把建立好的连接从内核中拿到应用程序里。如果客户端和服务器建立连接,服务器的应用程序不需要任何操作,系统内核直接完成了连接建立的流程(三次握手),完成流程后,就会在内核的队列中排队(每个ServerSocket都会有这个队列)。应用程序要想和这个客户端进行通信,就需要通过按accept方法,把内核队列里已经建立好连接的对象,拿到应用程序中。
符合生产者消费者模型
//通过accept方法,把内核中已经建立好的连接拿到应用程序中 //建立连接的细节流程,都是内核自动完成的,应用程序只需要用现成的 Socket clientSocket = serverSocket.accept();
返回的是一个Socket对象
ServerSocket专门用来接收连接, Socket类型的 clientSocket用来后续的客户端进行通信。
马路上招揽人头的销售 和 售楼部的员工
clientSocket.getInputStream(); clientSocket.getOutputStream();
InputStream和OutputStream就是字节流,TCP是传输内容同样是字节流。借助这两个对象,完成数据的“发送”和“接收”。
通过InputStream进行read操作,就是“接收”
通过OutputStream进行write操作,就是“发送”
3.文件资源泄露
由于DatagramSocket和ServerSocket在程序中,只有一个对象,生命周期都是贯穿整个程序的。随时有请求过来,都会使用到。不涉及到一直频繁申请导致的泄露问题。
但是clientSocket,每个循环中,每有一个新的客户端来建立连接,都会创建出新的clientSocket。并且这个Socket最多使用到该客户端断开连接。如果此时有很多客户端频繁建立连接,就会出现文件资源泄露的问题。
try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) {
这里只是关闭了clientSocket自带的流对象,并没有关闭本身。需要手动进行关闭
}finally { clientSocket.close(); //进行clientSocket的关闭 //processConnection方法就是在处理一个连接,这个方法执行完毕,连接就处理完了。 }
4.TCP回显服务器
服务器
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) { //通过accept方法,把内核中已经建立好的连接拿到应用程序中 //建立连接的细节流程,都是内核自动完成的,应用程序只需要用现成的 Socket clientSocket = serverSocket.accept(); processConnection(clientSocket); } } /** * 通过这个方法,来处理当前的连接 * * @param clientSocket */ public void processConnection(Socket clientSocket) throws IOException { //1.进入方法后,先打印日志,表示有客户端连接 System.out.printf("[%s,%d] 客户端上线\n", clientSocket.getInetAddress(), clientSocket.getPort()); //2.进行数据交互 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; } String request = scanner.next();//以空白符为本次读取字节流结束的标记 //1.读取请求并解析 String response = process(request); //2.根据请求,计算响应 //3.把响应写回到客户端: //可以把String转成字节数组,写进 OutputStream //也可以使用PrintWriter把OutputStream包裹一下,写进字符串 PrintWriter printWriter = new PrintWriter(outputStream); //此处的println是写入到outputStream对应的流对象中,也就是写入到clientSocket里面, // 数据就通过网络发送出去了。发送给当前连接的另外一端。 //因为之前连接本身就记录了对方的地址和端口,在写数据时直接写数据内容即可,不需要手动指定发给谁。 printWriter.println(response);//写就是输出的体现形式 //此处使用println带有\n,也是为了后续客户端可以使用Scanner.next来读取数据 printWriter.flush(); //同时,需要刷新缓冲区,确保数据从内存中写进网卡 System.out.printf("[%s:%d] req = %s ,resp = %s\n", clientSocket.getInetAddress(),clientSocket.getPort(),request,response); } } catch (IOException e) { throw new RuntimeException(e); }finally { clientSocket.close(); //进行clientSocket的关闭 //processConnection方法就是在处理一个连接,这个方法执行完毕,连接就处理完了。 } } public String process(String request){ //回显服务器 return request; } public static void main(String[] args) throws IOException { TcpEchoServer server = new TcpEchoServer(9090); server.start(); } } 服务器启动 [/127.0.0.1,63510] 客户端上线 [/127.0.0.1:63510] req = 你好 ,resp = 你好 [/127.0.0.1:63510] req = hello ,resp = hello [/127.0.0.1:63510] 客户端下线 [/127.0.0.1,63523] 客户端上线 [/127.0.0.1:63523] req = 6666666 ,resp = 6666666
客户端
public class TcpEchoClient { private Socket socket = null; public TcpEchoClient(String serverIp, int serverPort) throws IOException { //需要在创建Socket的同时,和服务器“建立连接”,告诉Socket,服务器在哪。 //当new这个对象时,操作系统的内核就会完成三次握手的具体细节,完成建立连接的过程 socket = new Socket(serverIp, serverPort); } public void start() { System.out.println("客户端启动"); Scanner scanner = new Scanner(System.in); try (InputStream inputStream = socket.getInputStream(); OutputStream outputStream = socket.getOutputStream()) { PrintWriter writer = new PrintWriter(outputStream); Scanner scannerNetwork = new Scanner(inputStream); while (true) { //1.从控制台读取用户输入的内容 System.out.println("->"); String request = scanner.next(); //2.把字符串作为请求,发送给服务器 writer.println(request); //客户端发的时候有换行,和服务器的scanner.next匹配 writer.flush(); //3.从服务器读取响应 String response = scannerNetwork.next();//和服务器的PrintWrite.println匹配 //4.把响应打印到界面 System.out.println(response); } } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws IOException { TcpEchoClient client = new TcpEchoClient("127.0.0.1",9090); client.start(); } } 客户端启动 -> 6666666 6666666 ->
如果同时启动两个客户端,同时连接服务器。先启动的客户端正常运行,另一个后启动的客户端,无法与服务器进行交互
在第一个客户端过来后,accept就返回得到了一个clientSocket,进入了processConnection方法。又进入了一个while循环,反复处理客户端发来的请求数据,如果客户端没发请求,服务器的代码就会阻塞在scanner.hasNext。此时第二个客户端也过来建立连接,连接建立成功后,连接对象就会在内核的队列里面,等待accept把连接取出来,在代码中处理。此时无法第一时间执行到第二次accept
第一个循环是循环获取连接,第二个循环是循环获取请求。第一个客户端就会使服务器处于processConnection方法内部, 此时卡在了方法中的循环,无法第二次执行accept方法。只有第一个客户端退出, 方法中的循环才能结束,从而第二次执行 accept
- 要解决这个问题,就要在处理第一个客户端请求的过程中,让代码能够快速的第二次执行accept
让两个循环能够“并发”执行,各执行各的,不会因为进入循环而影响另一个循环。所以,需要创建一个新的线程,由线程来执行processConnection方法。主线程就可以继续执行下次accept。新线程负责processConnection方法内部的循环。每有一个客户端,就要分配一个线程。
一个人是无法同时完成拉客 和 介绍楼盘的工作的
public void start() throws IOException { System.out.println("服务器启动"); while (true) { //通过accept方法,把内核中已经建立好的连接拿到应用程序中 //建立连接的细节流程,都是内核自动完成的,应用程序只需要用现成的 Socket clientSocket = serverSocket.accept(); //直接执行processConnection方法,会导致服务器不能处理客户端 //创建线程调用。 Thread thread = new Thread(()->{ try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } }); thread.start(); } }
新的线程负责在processConnection里面来循环处理客户端的请求。
- 如果有很多客户端,频繁的建立、断开连接。就会导致服务器频繁的创建销毁线程,造成大量开销。可以使用线程池来进行优化。
public void start() throws IOException { System.out.println("服务器启动"); ExecutorService service = Executors.newCachedThreadPool(); while (true) { //通过accept方法,把内核中已经建立好的连接拿到应用程序中 //建立连接的细节流程,都是内核自动完成的,应用程序只需要用现成的 Socket clientSocket = serverSocket.accept(); //直接执行processConnection方法,会导致服务器不能处理客户端 //创建线程调用。 // Thread thread = new Thread(()->{ // try { // processConnection(clientSocket); // } catch (IOException e) { // throw new RuntimeException(e); // } // }); // thread.start(); //使用线程池进行优化 service.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { throw new RuntimeException(e); } } }); } }
- 但是如果出现巨量的线程。可以用协程来解决。除了携程,可以使用IO多路复用/IO多路转接的方法来处理(用一个线程,同时处理多个客户端的socket)->NIO