概述
上节回顾
套接字,TCP版本的套接字 API
ServerSocket 服务器这边使用的
Socket: 服务器和客户端都需要使用对于服务器来说:
1.创建ServerSocket 关联上一个端口号 (称为listenSocket)
2.调用ServerSocket 的 accept 方法
accept的功能是把一个内核建立好的连接给拿到代码中处理
accpept会返回一个Socket实例,称为clientSocket
3.使用clientSocket的getlnputStream和getOutputStream
得到字节流对象,就可以进行读取写入了
4.当客户端断开连接之后,服务器就应该要及时的关闭clientSocket.(否则可能会出现文件资源泄漏情况)
对于客户端:
1.创建一个Socket对象.创建的同时指定服务器的ip和端口(这个操作就会让客户端和服务器建立TCP连接.这个连接建立的过程就是传说中的"三次握手",这个流程是内核完成的,用户代码感知不到)
2.客户端就可以通过Socket对象的getlnputStream和getOutputStream来和服务器进行通信了
在上一篇的TCP代码中,其实还存在一个很严重的bug!!!
实际开发中,一个服务器应该要对应很多个客户端!!而且升值是成千上万个.
现在我们在IDEA上再启动一个客户端
现在这里就有两个客户端了
当前就发现,当启动第二个客户端的时候,服务器就没有提示"上线"
当在第二个客户端发送数据的时候,发现没有任何反应
当退出第一个客户端的时候,神奇的事情出现了!!!服务器提示了客户端2上线,也得到了hello2这样响应!!
总结:当前咱们的服务器同一时刻,只能给一个客户端提供服务,只有前一个客户端下了,下一个客户端才能上来.这样的设定,显然是不科学的.
原因是啥?
解决方案:
使用多线程!
主线程里面循环调用 accept 每次获取到一个连接,就创建一个线程,让这个线程来处理这个连接
服务器代码改进
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; /** * Created with IntelliJ IDEA. * Description: * User: 灯泡和大白 * Date: 2022-07-26 * Time: 18:36 */ //这个代码和前面普通的 TCP 回显服务器,基本差不多,只不过,这里面增加了多线程的处理 //针对每个客户端都搞一个新的线程 public class TcpThreadEchoServer { private ServerSocket listenSocket = null; public TcpThreadEchoServer(int port) throws IOException { listenSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动"); while (true) { // 在这个代码中, 通过创建线程, 就能保证 accept 调用完毕之后, 就能立刻再次调用 accept . Socket clientSocket = listenSocket.accept(); // 创建一个线程来给这个客户端提供服务 Thread t = new Thread() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }; t.start(); } } // 这个代码和前面是一样的了. public void processConnection(Socket clientSocket) throws IOException { String log = String.format("[%s:%d] 客户端上线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(log); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { while (true) { // 1. 读取请求并解析 Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { log = String.format("[%s:%d] 客户端下线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(log); break; } String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回到客户端 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); printWriter.flush(); log = String.format("[%s:%d] req: %s; resp: %s", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); System.out.println(log); } } catch (IOException e) { e.printStackTrace(); } finally { clientSocket.close(); } } // 回显服务器, 直接把请求返回即可 public String process(String request) { return request; } public static void main(String[] args) throws IOException { TcpThreadEchoServer server = new TcpThreadEchoServer(9090); server.start(); } }
实际开发中,客户端的数量可能会很多啊.
这个时候能行吗?
虽然线程比进程更轻量,但是如果有很多客户端连接又退出,这就会导致咱们当前的服务器里频繁的创建销毁线程.....
这个时候还是会有很大的成本的
如何改进这个问题?
线程池
线程池版本
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; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class TcpThreadPoolEchoServer { private ServerSocket listenSocket = null; public TcpThreadPoolEchoServer(int port) throws IOException { listenSocket = new ServerSocket(port); } public void start() throws IOException { System.out.println("服务器启动!"); ExecutorService executorService = Executors.newCachedThreadPool(); while (true) { Socket clientSocket = listenSocket.accept(); // 使用线程池来处理当前的 processConnection executorService.submit(new Runnable() { @Override public void run() { try { processConnection(clientSocket); } catch (IOException e) { e.printStackTrace(); } } }); } } public void processConnection(Socket clientSocket) throws IOException { String log = String.format("[%s:%d] 客户端上线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(log); try (InputStream inputStream = clientSocket.getInputStream(); OutputStream outputStream = clientSocket.getOutputStream()) { while (true) { // 1. 读取请求并解析 Scanner scanner = new Scanner(inputStream); if (!scanner.hasNext()) { log = String.format("[%s:%d] 客户端下线!", clientSocket.getInetAddress().toString(), clientSocket.getPort()); System.out.println(log); break; } String request = scanner.next(); // 2. 根据请求计算响应 String response = process(request); // 3. 把响应写回到客户端 PrintWriter printWriter = new PrintWriter(outputStream); printWriter.println(response); printWriter.flush(); log = String.format("[%s:%d] req: %s; resp: %s", clientSocket.getInetAddress().toString(), clientSocket.getPort(), request, response); System.out.println(log); } } catch (IOException e) { e.printStackTrace(); } finally { clientSocket.close(); } } // 回显服务器, 直接把请求返回即可 public String process(String request) { return request; } }
假设极端情况下,一个服务器面临很多很多客户端,这些客户端,连上之后,并没有退出.
这个时候服务器这边同一时刻,就会存在很多很多线程(上万个线程)
这个情况,会有其他的问题嘛?这是科学的处理方法吗?
实际上,如果出现这样的情况,是不科学的!!!
每个线程都要占据一定的系统资源,如果线程太多太多,此时很多系统资源就会非常紧张,达到一定程度,及其可能就扛不住了(宕机了)
系统资源指的是:内存资源,CPU资源
针对这种线程特别多的情况,如何改进呢?
1.可以使用协程来代替线程,完成并发.(很多协程的实现是 M:N) 协程比线程还轻量(GO语言)
2.可以使用IO多路复用的机制,完成并发.
IO多路复用:从根本上解决服务器处理高并发的问题.
在内核里来支持这样的一个功能
刚才假设有1万个客户端,在服务器这边会用一定的数据结构来把这1万个客户端对应的socket都存好.不需要一个线程对应一个客户端了,就一共只有一个/几个线程
IO多路复用机制,就能够做到,那个socket上面有数据了,就通知到应用程序,让这个线程来从这个socket中读数据.
3.使用多个主机(分布式)
提供更多的硬件资源
这三点都是一个基本的来处理一个高并发场景,所使用的的办法
网络通信的原理
网络协议是分层的.
应用层:
应用层协议,是程序猿打交道的最多的协议.
应用层是直接和程序相关的
1.使用现成的应用层协议来进行开发.
2.程序猿自己定制一个协议,完成需求
协议不是一成不变的.协议也不是遥不可及的.协议很多时候都是程序猿自己约定的.
只要客户端 + 服务器都是自己开发的,这个时候中间使用啥样的协议,完全是咱们自己说了算!!!
现成的应用层协议:
其实也是有很多的.
用的最多的应用层协议HTTP协议.
比如,我在浏览器上,输入一个地址,然后浏览器打开了一个网页.(如果电脑没网,能打的开嘛)
这个过程就是,客户端(浏览器)给bing的服务器发送了一个请求,请求中就包含另一个链接.
然后bing的服务器给浏览器返回了一个响应,这个响应就是一个网页.
在这个网络通信中,使用的应用层协议,就是HTTP协议.(注意,当前很多网站,其实是使用了HTTPS,HTTPS也是基于HTTP).
再比如,手机端,打开了一个饿了么/美团外卖,先看到一个商家列表,点进去,就能看到吃的列表.还有下单,支付......这些过程客户端和服务器之间的通信,大概率也是在使用HTTP协议.
除了HTTP之外,还有很多其他的应用层协议
FTP.现在已经很少见了.以前的时候,文件传输,就可以使用
SSH:后面linux会涉及到.(使用一个终端软件(例如xahell),就可以连接到一个服务器)(在一个遥远的机房里面)
TELNET:现在不常见,如果进行一些嵌入式开发.....
DNS:域名解析的协议
........
应用层协议种类非常多.....
自定义协议,程序猿自己来约定.
约定,请求是啥格式.
响应是啥格式.
客户端和服务器之间就可以按照这样的约定来进行开发了.
(这个事在日常工作中经常会涉及)
假设开发一个外卖软件.
客户端(APP):这一波程序猿开发客户端
服务器:这一波程序猿开发服务器
假设现在有一个需求:要求在外卖软件的首页,就能显示一个"优惠活动",用户参与活动,就能抽取红包.
客户端:就得修改界面,能够显示优惠活动的详情.
服务器:修改逻辑后,针对啥样的用户能参加优惠,具体咋样能领到红包,红包金额是多少...
这个时候客户端启动的时候,就要向服务器查询,当前是否能够参与活动.
服务器就要返回"是"/"否"
这里就要求,客户端和服务器必须统一!!!
最后
以上就是可爱铅笔为你收集整理的【Java成王之路】EE初阶第十篇:(网络编程) 4的全部内容,希望文章能够帮你解决【Java成王之路】EE初阶第十篇:(网络编程) 4所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复