客户端&服务器访问方式的演进

    技术2022-07-10  88

    目录

    一、网络编程基本知识

    第一种方式:BIO/OIO 

    二、网络编程基本实例

    1、客户端发一句话给服务端

    2、客户端给服务端发一个文本

    3、客户端给服务端发送完消息后,服务端再给客户端一个回馈

    三、URL编程


    一、网络编程基本知识

    前言:

            端口号与IP地址的组合得出一个网络套接字:Socket,网络编程通常称为socket编程。

    1、网络模型:

        a.通讯方式:TCP、UDP

            i. TCP : 可靠连接,使命必达,速度慢

            ii. UDP : 不可靠,速度快

    2、 TCP的编程模型

        a. BIO / OIO

            i. Blocking IO / Old IO

        b. NIO(linux支持)

             i. New IO : Non-Blocking IO

             ii. Selector

             iii. ByteBuffer (single pointer)

        c. AIO(仅仅windows支持)

             i. Asynchronous IO

    2、Netty

        a. 封装了NIO ByteBuf(read pointer、writer pointer)

    第一种方式:BIO/OIO 

    写一个简单的例子:客户端连接服务器,只写一句话

    第一步:创建客户端和服务端,并让客户端连接上服务端

    server:

    ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8899)); System.out.println("hello"); Socket accept = serverSocket.accept(); System.out.println("world");

    client:

    Socket socket = new Socket("localhost",8899);

    解析:

    在以上的server端的代码中,运行以后”hello“会输出出来,但是,accept()方法是阻塞的,也就是说,如果客户端没有启动,或者说没有客户端连接到服务端,此时”world不会输出出来“。客户端启动,则”world“就会输出出来。

    第二步:

    客户端连接上服务端传给服务端一些信息

    server: ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8899)); System.out.println("hello"); Socket accept = serverSocket.accept(); System.out.println("world"); // 获取输入流 InputStream is = accept.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String str = br.readLine(); System.out.println(str); // 关闭资源 is.close(); accept.close(); serverSocket.close(); client: Socket socket = new Socket("localhost",8899); OutputStream oos = socket.getOutputStream(); // 获取输出流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(oos)); bw.write("你好啊,我是Michael"); bw.newLine(); bw.flush(); // 关闭资源 bw.close();

    解析:此时服务端不仅仅会输出”hello“、”world“还会输出客户端传过来的信息:”你好啊,我是Michael“.

    此时server中的br.readLine()是一个阻塞方法,即:如果客户端只是连接上,但是没有传过来数据,服务端会在br.readLine()处阻塞。如第三步所示

    第三步:

    server端的代码不变

    client代码如下:

    Socket socket = new Socket("localhost",8899); OutputStream oos = socket.getOutputStream(); // 获取输出流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(oos)); // 下面的代码是等待键盘输入内容,如果不输入内容,下面的代码会进入阻塞状态 System.in.read();

    解析:此时启动客户端,服务端仅仅会输出"hello"、"world"。并进入阻塞状态。

    这种情况其实是一种网络攻击,叫拒绝服务攻击,也就是说我连接上你的服务器,但就是什么事也不干,其他正常的连接也连接不上来,这就是一种攻击。

    这种模式类似于:一个餐厅开门后只能服务于一个客户,客户吃完饭,餐厅会关门,如果需要对下一个客户提供服务,需要重新开张。

    以上模式只能接受一个客户端。如何改进呢:

    第一种改进方式:

    server: ServerSocket serverSocket = new ServerSocket(); serverSocket.bind(new InetSocketAddress(8899)); boolean started = true; while (started){ Socket accept = serverSocket.accept(); // 获取输入流 InputStream is = accept.getInputStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is)); String str = br.readLine(); System.out.println(str); // 关闭资源 is.close(); accept.close(); } serverSocket.close(); client Socket socket = new Socket("localhost",8899); OutputStream oos = socket.getOutputStream(); // 获取输出流 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(oos)); bw.write("你好啊,我是Michael"); bw.newLine(); bw.flush(); bw.close();

    解析:启动服务端,重复启动客户端,没启动一起客户端,服务端会收到"你好啊,我是Michael"。这种模式类似于,饭店每次只能允许一个人在里面吃饭,此时其他想要吃饭的客户必须得等上一个客户吃完饭走出去餐厅,才能进去。也就是其他客户端会进入阻塞模式。

    第二种改进方式:

    这种方式为每一个客户端生成一个线程,当大量客户端同时访问此服务时,由于起的线程太多,服务基本上就挂了

    public class Server { public static void main(String[] args) throws Exception { ServerSocket socket = new ServerSocket(); socket.bind(new InetSocketAddress("localhost", 8888)); boolean started = true; while (started) { new Thread(()->{ try { Socket s = socket.accept(); BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream())); String str = br.readLine(); System.out.println(str); br.close(); s.close(); } catch (IOException e) { e.printStackTrace(); } }).start(); } socket.close(); } } public class Client { public static void main(String[] args) throws IOException { Socket s = new Socket("localhost", 8888); BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream())); bw.write("mashibing"); bw.newLine(); bw.flush(); bw.close(); } }

    这种方式类似于:每个顾客去餐厅吃饭时,餐厅都找一个属于这个顾客的服务员(也就是线程)去为他服务,不妨碍其他顾客吃饭。而主线程的作用就是为其分配服务员(线程)。

    第二种方式:NIO

    NIO中使用的是ServerSocketChannel模式,channel就是通道的意思

    public class Server2 { public static void main(String[] args) throws IOException { ServerSocketChannel ssc = ServerSocketChannel.open(); ServerSocket socket = ssc.socket(); socket.bind(new InetSocketAddress("localhost",8899)); // 将此通道设置为非阻塞式 ssc.configureBlocking(false); System.out.println("Server started, listening on:" + ssc.getLocalAddress()); // 启动大管家 Selector selector = Selector.open(); // 我让大管家管什么事呢?让大管家负责有连接需要连接过来的服务 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true){ // 开始轮询,有事情就会处理事情,在轮询时是阻塞状态 selector.select(); Set<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> it = keys.iterator(); while (it.hasNext()){ SelectionKey key = it.next(); it.remove(); handle(key); } } } private static void handle(SelectionKey key) { // 客户端连接的情况 if(key.isAcceptable()){ try { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); // 有客户端进来时单独建立一个新的通道,同时也将此通道设置为非阻塞式 SocketChannel sc = ssc.accept(); sc.configureBlocking(false); // 此通道注册一个读数据的通道还是什么不清楚 sc.register(key.selector(),SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } finally { } }else if(key.isReadable()){ SocketChannel sc = null; try { sc = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(512); buffer.clear(); int len = sc.read(buffer); if(len!=-1){ System.out.println(new String(buffer.array(),0,len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); } catch (IOException e) { e.printStackTrace(); } finally { if(sc!=null){ try { sc.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }

    以上代码是NIO代码,只用了一个线程。Nertty就是对其的封装。NIO使用了ByteBuffer,其底层封装了一个字节数组,和一个指针,记录了读写数据的位置,但是它不好用,也容易出错。而Netty底层使用了ByteBuf,也是一个字节数组,但是其有两个指针分别记录读写数据的位置,并且,Netty可以操作直接内存即零拷贝,不需要经过JDK的内存,直接使用操作系统的内存,少了拷贝数据的过程,增加效率。

    此种模型因为是单线程的,当大管家在处理某一张桌子(客户端)的需求时,其他需求都需要排队等待,改进方式如下:

    NIO另一种模型:一个大管家+多个服务员(线程),线程数固定,大管家只负责接客,服务员负责每个客人的业务需求。服务员忙完后回到线程池进入等待状态,有了新的客户再去服务。但是ByteBuffer的问题依然没有解决

    Netty:

    以下只有服务端代码,没有客户端代码,客户端代码可以使用BIO中的客户端

    public class NettyServer { public static void main(String[] args) throws InterruptedException { // 负责接客 NioEventLoopGroup bossGroups = new NioEventLoopGroup(2); // 负责服务 NioEventLoopGroup workerGroup = new NioEventLoopGroup(4); // Server启动辅助类 ServerBootstrap b = new ServerBootstrap(); b.group(bossGroups,workerGroup); // 异步全双工 b.channel(NioServerSocketChannel.class); // netty 帮我们内部处理accept的过程 b.childHandler(new MyChildInitializer()); b.bind(8888).sync(); } } class MyChildInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { System.out.println("a client connected !"); } }

    一个完整的Netty例子

    public class NettyServer { public static void main(String[] args) throws InterruptedException { // 负责接客 NioEventLoopGroup boosGroup = new NioEventLoopGroup(2); // 负责服务 NioEventLoopGroup workerGroup = new NioEventLoopGroup(4); // server启动辅助类 ServerBootstrap b = new ServerBootstrap(); b.group(boosGroup,workerGroup); // 指定使用的channel类型:异步全双工 b.channel(NioServerSocketChannel.class); b.childHandler(new MyChildInitializer2()); ChannelFuture future = b.bind(8899).sync(); // 优雅的关闭方式 future.channel().closeFuture().sync(); boosGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } class MyChildInitializer2 extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new MyChildHandler2()); } } class MyChildHandler2 extends ChannelInboundHandlerAdapter{ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; System.out.println(buf.toString()); ctx.writeAndFlush(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); } } public class NettyClient { public static void main(String[] args) throws InterruptedException, IOException { NioEventLoopGroup worker = new NioEventLoopGroup(1); Bootstrap bs = new Bootstrap(); bs.group(worker); bs.channel(NioSocketChannel.class); bs.handler(new MyChannelInit()); ChannelFuture future = bs.connect("localhost", 8899).sync(); // 等待关闭 future.channel().closeFuture().sync(); System.out.println("go on"); worker.shutdownGracefully(); } } class MyChannelInit extends ChannelInitializer<SocketChannel>{ @Override protected void initChannel(SocketChannel socketChannel) throws Exception { socketChannel.pipeline().addLast(new MyHandler()); } } class MyHandler extends ChannelInboundHandlerAdapter{ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println(msg.toString()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ByteBuf buf = Unpooled.copiedBuffer("mashbing".getBytes()); ctx.writeAndFlush(buf); } }

    详情请看马士兵关于线程与高并发的视频

    二、网络编程基本实例

    1、客户端发一句话给服务端

    public class client { public static void main(String[] args) { Socket socket = null; OutputStream oos = null; try { // 1、创建socket对象,指明服务器端的ip和端口号 InetAddress address = InetAddress.getByName("localhost"); socket = new Socket(address, 8899); // 2、获取一个输出流,用于输出数据 oos = socket.getOutputStream(); // 3、写出数据的操作 oos.write("你好啊,我是client".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { // 4、资源关闭 try { oos.close(); } catch (IOException e) { e.printStackTrace(); } try { socket.close(); } catch (IOException e) { e.printStackTrace(); } } } } public class SeverTest { public static void main(String[] args) throws IOException { ServerSocket ss = null; Socket socket = null; InputStream is = null; ByteArrayOutputStream baos = null; try { // 1、创建服务器端的ServerSocket,指明自己的端口号 ss = new ServerSocket(8899); // 2、调用accept方法,表示接受客户端的socket socket = ss.accept(); // 3、获取输入流 is = socket.getInputStream(); // // 不建议这个方式: // byte[] bytes = new byte[10]; // int len; // while ((len = is.read(bytes)) != -1) { // String str = new String(bytes,0,len); // System.out.println(str); // } // 4、读取输入流中的数据 // ByteArrayOutputStream内部自己封装了一个字节数组,也会自动扩容,使用这个时,会先将数据写入自己的字节数组中。 baos = new ByteArrayOutputStream(); byte[] bytes = new byte[10]; int len; while ((len = is.read(bytes)) != -1) { baos.write(bytes, 0, len); } System.out.println(baos.toString()); System.out.println("收到了来自于:" + socket.getInetAddress().getHostAddress() + "的数据"); } catch (IOException e) { e.printStackTrace(); } finally { is.close(); ss.close(); baos.close(); socket.close(); } } }

    2、客户端给服务端发一个文本

    public class client1 { public static void main(String[] args) throws Exception { Socket socket = null; OutputStream oos = null; FileInputStream fis = null; try { socket = new Socket(InetAddress.getByName("localhost"), 8899); oos = socket.getOutputStream(); fis = new FileInputStream(new File("hello.txt")); byte[] bytes = new byte[1024]; int len; while ((len = fis.read(bytes)) != -1) { oos.write(bytes,0,len); } System.out.println("文件写入成功"); } catch (IOException e) { e.printStackTrace(); } finally { socket.close(); oos.close(); fis.close(); } } } public class server1 { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; Socket socket = null; InputStream is = null; FileOutputStream fos = null; try { serverSocket = new ServerSocket(8899); socket = serverSocket.accept(); is = socket.getInputStream(); fos = new FileOutputStream(new File("b.txt")); byte[] bytes = new byte[1024]; int len; while ((len=is.read(bytes))!=-1){ fos.write(bytes,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { serverSocket.close(); socket.close(); is.close(); fos.close(); } } }

    3、客户端给服务端发送完消息后,服务端再给客户端一个回馈

    public class client2 { public static void main(String[] args) throws Exception { Socket socket = null; OutputStream oos = null; FileInputStream fis = null; ByteArrayOutputStream baos = null; try { socket = new Socket(InetAddress.getByName("localhost"), 8899); oos = socket.getOutputStream(); fis = new FileInputStream(new File("hello.txt")); byte[] bytes = new byte[1024]; int len; while ((len = fis.read(bytes)) != -1) { oos.write(bytes,0,len); } // 此语句的作用是告诉客户端我的消息发送停止了,如果没有此语句, // 因为server的read方法是阻塞的,所以会erver会一直在读数据 socket.shutdownOutput(); System.out.println("文件写入成功"); InputStream is = socket.getInputStream(); baos = new ByteArrayOutputStream(); byte[] bytes1 = new byte[1024]; int len2; while ((len2=is.read(bytes1))!=-1){ baos.write(bytes1,0,len2); } System.out.println(baos.toString()); } catch (IOException e) { e.printStackTrace(); } finally { fis.close(); oos.close(); socket.close(); baos.close(); } } } public class server2 { public static void main(String[] args) throws IOException { ServerSocket serverSocket = null; Socket socket = null; InputStream is = null; FileOutputStream fos = null; OutputStream oos = null; try { serverSocket = new ServerSocket(8899); socket = serverSocket.accept(); is = socket.getInputStream(); fos = new FileOutputStream(new File("c.txt")); byte[] bytes = new byte[1024]; int len; while ((len=is.read(bytes))!=-1){ fos.write(bytes,0,len); } System.out.println("图片传输完成"); oos = socket.getOutputStream(); oos.write("你好,信的内容我已经收到了,很好,不错!".getBytes()); } catch (IOException e) { e.printStackTrace(); } finally { fos.close(); serverSocket.close(); socket.close(); is.close(); oos.close(); } } }

    三、URL编程

    public class URLTest { public static void main(String[] args) throws MalformedURLException { URL url = new URL("https://www.bilibili.com/video/BV1Kb411W75N?p=629"); // 获取该URL的协议名 System.out.println(url.getProtocol()); // 获取该URL的主机名 System.out.println(url.getHost()); // 获取该URL的端口号 System.out.println(url.getPort()); // 获取该URL的文件路径 System.out.println(url.getPath()); // 获取该URL的文件名 System.out.println(url.getFile()); // 获取该URL的查询名 System.out.println(url.getQuery()); } } public class URLTest02 { public static void main(String[] args) throws IOException { URL url = new URL("\"https://www.bilibili.com/video/BV1Kb411W75N?p=629\""); HttpURLConnection urlConnection = null; InputStream is = null; FileOutputStream fos = null; try { urlConnection = (HttpURLConnection) url.openConnection(); urlConnection.connect(); is = urlConnection.getInputStream(); fos = new FileOutputStream("d.txt"); byte[] bytes = new byte[1024]; int len; while ((len=is.read(bytes))!=-1){ fos.write(bytes,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { // 关闭资源 is.close(); fos.close(); urlConnection.getClass(); } } }

    Processed: 0.018, SQL: 9