网络编程及回显服务器

打印 上一主题 下一主题

主题 850|帖子 850|积分 2550



网络编程

网络编程的目标:写一个应用程序,让这个应用程序可以使用网络通信.(须要调用传输层提供的API)
UDP和TCP特点对比

UDP:无连接、不可靠传输,面向数据报,全双工.
TCP:有连接、可靠传输、面向字节流、全双工.
有无连接

可靠传输与不可靠传输


可靠传输不是说 a 给 b 发的消息100%都能到.(这个要求太难了)
可靠传输指的是:a 尽大概的把消息传给 b 并且在传输失败的时候,a 能感知到,或者在传输成功的时候,也能知道自己传输成了.
TCP 是可靠传输(传输服从就低沉了)
UDP 是不可靠传输(传输服从更高)
所以,无论是可靠传输还是不可靠传输,这都是他们的特点,是不分优劣的.
TCP 是可靠传输,UDP 是不可靠传输,因此 TCP 比 UDP 更安全???
注意,这句话是错误的,谈到"网络安全"指的是,如果你传输的数据是否容易被黑客截获,以及如果被截获后,是否会泄漏一些紧张信息?(关键词:安全、入侵、破解,加密,反编译...)
面向字节流与面向数据报

TCP 和文件操作类似都是"流"式的(由于这里传输的单位是字节,称为字节流)
UDP 是面向数据报,读写的基本单位是一个 UDP 数据报(包含了一系列的数据/属性)
UDP的socket api

两个核心的类
1.DatagramSocket

是一个 Socket 对象
操作系统使用文件这样的概念来管理一些软硬件资源,网卡,操作系统也是使用文件的方式来管理网卡的,表示网卡的这类文件称为 socket 文件.
Java中的socket 的对象就对应着系统里的 socket 文件,(终极还是要落到网卡)
要举行网络通信,必须得先有 socket 对象.

2.DatagramPacket

表示了一个 UDP 数据报.代表了系统中所设定的 UDP 数据报的二进制布局
getData():获取到 UDP 数据报载荷部分(也就是完整的应用层数据报)
回显服务器(echo server)(最简单的UDP服务器)

什么是回显服务器?客户端向服务器发送了什么,服务器就返回什么
这个非常是非常常见的非常,通常出现这个非常的缘故原由使端标语被占用,端标语是用来区分主机上的应用程序的,一个应用程序可以占据主机上的多个端口但一个端口只能被一个进程占用(这句话实在不够严谨,有特例,但是此处不做讨论),如果一个端标语已经被别人的进程占用了,此时你再尝试创建这个socket对象,占用该端口,此时就会报错.
一个服务器要给很多客户端提供服务,但是服务器也不知道客户端什么时候来,所以服务器只能"时刻预备着",随时客户端来了就能随时提供服务
服务器工作的3个核心

1.读取请求并解析


这里 receive 方法的参数 DatagramPacket 是一个输出型参数,传给 receive 的是一个空的对象,receive 内部就会把这个空对象的内容填充上,当 receive 执行竣事,于是就得到一个装满内容的 DatagramPacket

这个对象用来生存数据的内存空间,是须要手动指定的,不像我们之前学过的集合类,内部都是有自己管理内存(能够申请内存,开释内存,内存扩容)本领的,其中这个数组的巨细是可以任意写的,但是也不能写太大,最大不能超过64KB.
2.根据请求,盘算出相应

(回显服务器不关心这个流程,请求是什么?就返回什么),但一个贸易级的服务器,重要代码都是在完成这里的代码.
3.把相应写回客户端


在盘算这里的长度的时候,要注意:response.length():这个盘算的是这个字符串的长度,单位是字符,response.getBytes().length():这个盘算的是字符串中的内容的长度,单位是字节,而我们这儿socket API自己都是按照字节来处置惩罚的,别的,关于发送给谁的这个标题,我们固然是要发送给客户端的,而客户端的信息包含在requestPacket当中
  1. package netWork;
  2. import java.io.IOException;
  3. import java.net.DatagramPacket;
  4. import java.net.DatagramSocket;
  5. import java.net.SocketException;
  6. /**
  7. * Created with IntelliJ IDEA
  8. * Description:
  9. * User: lenovo
  10. * Date: 2024 -01 -21
  11. * Time: 11:00
  12. */
  13. //UDP的回显服务器,客户端发的是什么服务器就接受到什么
  14. public class UdpEchoServer {
  15.     private DatagramSocket socket=null;
  16.     //创建一个服务器,创建服务器的时候需要端口号
  17.     public UdpEchoServer(int port) throws SocketException {
  18.         socket=new DatagramSocket(port);
  19.     }
  20.     //用这个来启动服务器
  21.     public void start() throws IOException {
  22.         System.out.println("服务器启动!");
  23.         while(true){
  24.             //作为一个服务器,我们也不知道客户端什么时候来,所以需要这里长期等待
  25.             //客户端一旦来了,我们可能需要反复的长期的对客户端的数据进行一个处理
  26.             //1.读取请求并解析
  27.             DatagramPacket requestPacket=new DatagramPacket(new byte[4096],4096);
  28.             socket.receive(requestPacket);
  29.             //并要转换的前提是后续客户端发送的字符串是文本内容.
  30.             //这里的length是UDP数据报 payload 的长度
  31.             String receive=new String(requestPacket.getData(),0,requestPacket.getLength());
  32.             //2.根据请求,计算出响应
  33.             String response=response(receive);
  34.             //3.把响应写回客户端(发送的内容是什么?发送给谁?)
  35.             DatagramPacket res=new DatagramPacket(response.getBytes(),
  36.                     response.getBytes().length,requestPacket.getSocketAddress());
  37.             socket.send(res);
  38.             //记录日志,方便观察程序执行效果
  39.             System.out.printf("[%s,%d] req:%s  reqs %s\n",requestPacket.getAddress().toString(),
  40.             requestPacket.getPort(),receive,response);
  41.         }
  42.     }
  43.     public String response(String receive){
  44.         return receive;
  45.     }
  46.     public static void main(String[] args) throws IOException {
  47.         UdpEchoServer udpEchoServer=new UdpEchoServer(96);
  48.         udpEchoServer.start();
  49.     }
  50. }
复制代码
客户端工作的代码


服务器要固定到96这个端口

客户端要去访问96这个端口
如果此时启动多个客户端,多个客户端也可以被服务器应对
启动多个程序IDEA须要设置一下

  1. package netWork;
  2. import java.io.IOException;
  3. import java.net.*;
  4. import java.util.Scanner;
  5. /**
  6. * Created with IntelliJ IDEA
  7. * Description:
  8. * User: lenovo
  9. * Date: 2024 -01 -21
  10. * Time: 11:01
  11. */
  12. //UDP的回显客户端
  13. public class UdpEchoClient {
  14.     private DatagramSocket socket=null;
  15.     private String severIP;
  16.     private int severPort;
  17.     public UdpEchoClient(String Ip,int Port) throws SocketException {
  18.         severIP=Ip;
  19.         severPort=Port;
  20.         socket=new DatagramSocket();
  21.     }
  22.     public void start() throws IOException {
  23.         Scanner scanner=new Scanner(System.in);
  24.         System.out.println("客户端启动");
  25.         while(true){
  26.             //1.从控制台输入用户所需要的内容
  27.             System.out.print("->");
  28.             String request=scanner.next();
  29.             //2.构造请求对象并发给服务器
  30.             DatagramPacket requestPacket=new DatagramPacket(
  31.                     request.getBytes(),
  32.                     request.getBytes().length,
  33.                     InetAddress.getByName(severIP),severPort);
  34.             socket.send(requestPacket);
  35.             //3.读取服务器响应,并返回响应内容
  36.             DatagramPacket res=new DatagramPacket(new byte[4096],4096);
  37.             socket.receive(res);
  38.             String response=new String(res.getData(),0,res.getLength());
  39.             //4.显示在屏幕上
  40.             System.out.println(response);
  41.         }
  42.     }
  43.     public static void main(String[] args) throws IOException {
  44.         UdpEchoClient udpEchoClient=new UdpEchoClient("127.0.0.1",9696);
  45.         udpEchoClient.start();
  46.     }
  47. }
复制代码
总结程序执行流程


TCP的socket API

TCP传输的是字节流,一个字节一个字节举行传输的,所以一个 TCP 数据报就是一个字节数组byte[]
1.SeverSocket

给服务器使用的 socket
2.Socket

既会给服务器使用,也会给客户端使用.
TCP版的回显服务器

服务器代码

这里须要注意的是,进入 while 循环之后,tcp 要做的事变不是读取客户端的请求,而是先处置惩罚客户端的"连接".(这里说到的"连接"并不是"握手",握手是系统内核负责的,我们在写代码的过程中感知不到握手的过程,此处说的连接是握手之后得到的东西)(服务器内核里有很多客户端的连接,固然内核中的连接很多,但是在应用程序中还是得一个一个处置惩罚的,内核中的连接就像一个一个"代服务项"这些代服务项在一个队列数据布局中,应用程序就须要一个一个完成这些任务)要完成任务,就须要先取任务

把内核中的连接获取到应用程序中了,这个过程类似于"生产者斲丧者模子"

当服务器执行到 accept 的时候,此时客户端大概还没来,那么此时 accept 就会壅闭.
一次IO,重要是经历两个部分
1.等(壅闭)
2.拷贝数据

accept 是把内核中已经建立好的连接,拿到应用程序中。但是这内里的返回值并非是一个 connection 这样的对象,而是一个 socket 对象。这个 socket 对象就像一个耳麦一样,就可以说话,也能听到对方的声音(通过socket对象和对方举行网络通信,我们不消管对方到底是谁,冲着这个递过来的耳麦说话就行了)

IO操作是比较有开销的,相比于访问内存,举行IO次数越多,程序的速度就越慢.使用一块内存作为缓冲区,写数据的时候先写到缓冲区里攒一波数据,统一举行IO.
PrinWriter内置了缓冲区,手动革新,确保这里的数据是真的通过网卡发出去了.而不是残留在内存缓冲区中的.
这里我们加上flush更稳妥,不加也不一定出错!!缓冲区内置了一定的革新策略,好比缓冲区满了,就会触发革新;再好比,程序退出,也会触发革新.
到此,程序还存在两个标题.
1.在这个程序中涉及到两类Socket
1>SeverSocket(只有一个,生命周期跟随程序,不关闭也没事)
2>Socket

此处的Socket是在被反复创建的,就会出现文件资源泄漏.我们要确保,连接断开之后,Socket要可以被关闭

下面这样写也可以

这个有待考虑

  1. package TCP;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. import java.io.PrintWriter;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. import java.util.Scanner;
  9. /**
  10. * Created with IntelliJ IDEA
  11. * Description:
  12. * User: lenovo
  13. * Date: 2024 -01 -24
  14. * Time: 13:49
  15. */
  16. public class TcpEchoSever {
  17.     private ServerSocket serverSocket=null;
  18.     public TcpEchoSever(int port) throws IOException {
  19.         serverSocket=new ServerSocket(port);
  20.     }
  21.     public void start() throws IOException {
  22.         //从内核中的队列当中取任务
  23.         System.out.println("服务器开始工作!");
  24.         while(true){
  25.             Socket clientSocket=serverSocket.accept();
  26.             //处理连接的逻辑
  27.             procesConnection(clientSocket);
  28.             clientSocket.close();
  29.         }
  30.     }
  31.     //处理连接的逻辑
  32.     private void procesConnection(Socket clientSocket) {
  33.         System.out.printf("[%s:%d]",clientSocket.getInetAddress().toString(),clientSocket.getPort());
  34.         try(InputStream inputStream=clientSocket.getInputStream();
  35.             OutputStream outputStream= clientSocket.getOutputStream()){
  36.             while(true){
  37.                 Scanner scanner=new Scanner(inputStream);
  38.                 if(!scanner.hasNext()){
  39.                     System.out.println("服务器关闭!");
  40.                     break;
  41.                 }else{
  42.                     //1.读取数据,只有在传过来的数据是文本状态时才可以这么做
  43.                     //这个代码暗藏一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割
  44.                     //空白符:空格 换行 回车 制表符 垂直制表符 等等
  45.                     String request=scanner.next();
  46.                     //2.处理请求
  47.                     String rec=processrequest(request);
  48.                     //3.发送数据,由于这里是TCP协议,所以要用字节流来发送,scanner是字符流
  49.                     PrintWriter printWriter=new PrintWriter(outputStream);
  50.                     //这里使用println是为了有空白符(在结尾加上\n,方便客户端使用scanner.next来读取响应).
  51.                     //网络程序讲究的就是客户端和服务器能够配合!
  52.                     printWriter.println(rec);
  53.                     //这个操作是为了刷新缓区.
  54.                     printWriter.flush();
  55.                     //日志,在屏幕上显示一下
  56.                     System.out.printf("[%s;%d] %s %s",clientSocket.getInetAddress().toString(),
  57.                             clientSocket.getPort(),request,rec);
  58.                 }
  59.             }
  60.         }catch (IOException e){
  61.             e.printStackTrace();
  62.         }
  63.     }
  64.     private String processrequest(String request) {
  65.         return request;
  66.     }
  67.     public static void main(String[] args) throws IOException {
  68.             TcpEchoSever tcpEchoSever=new TcpEchoSever(3060);
  69.             tcpEchoSever.start();
  70.     }
  71. }
复制代码
客户端代码

客户端要做的事变:
1.从控制台读取用户的输入
2.把输入的内容构造成请求并发送给服务器
3.从服务器读取相应
4.把相应表现到控制台上

不是每个流对象都持有文件形貌符,持有文件形貌符,是要调用操作系统提供的open方法,(系统调用,是要在内核中完成的,是一件相称严厉/重量的事变)
  1. package TCP;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. import java.io.PrintWriter;
  6. import java.net.Socket;
  7. import java.util.Scanner;
  8. /**
  9. * Created with IntelliJ IDEA
  10. * Description:
  11. * User: lenovo
  12. * Date: 2024 -01 -24
  13. * Time: 13:50
  14. */
  15. public class TcpEchoClient {
  16.     private Socket socket=null;
  17.     public TcpEchoClient(String severIP,int port) throws IOException {
  18.         socket=new Socket(severIP,port);
  19.     }
  20.     public void start(){
  21.         Scanner scanner=new Scanner(System.in);
  22.         System.out.println("客户端开始工作!");
  23.         try(InputStream inputStream=socket.getInputStream();
  24.             OutputStream outputStream=socket.getOutputStream()){
  25.             while(true){
  26.                 System.out.print("->");
  27.                 //1.从控制台读取用户的输入
  28.                 String request=scanner.next();
  29.                 //2.把输入的内容构造成请求并发送给服务器
  30.                 PrintWriter printWriter=new PrintWriter(outputStream);
  31.                 printWriter.println(request);
  32.                 //不要忘记flush,确保数据真的发送出去
  33.                 printWriter.flush();
  34.                 //3.从服务器读取响应
  35.                 Scanner scannerGet=new Scanner(inputStream);
  36.                 String res=scannerGet.next();
  37.                 //4.把响应打印出来
  38.                 System.out.println(res);
  39.             }
  40.         }catch (IOException e){
  41.             e.printStackTrace();
  42.         }
  43.     }
  44.     public static void main(String[] args) throws IOException {
  45.         TcpEchoClient tcpEchoClient=new TcpEchoClient("127.0.0.1",3060);
  46.         tcpEchoClient.start();
  47.     }
  48. }
复制代码
这样写固然可以正确的运行程序,但是这样写是有标题的,这样写服务器只能接收到一个客户端发来的消息,为什么?

在TCP编程中,必须要先有连接,我们要先取出连接才可以举行对数据的处置惩罚,所以当客户端1在执行的时候,代码会一直在while循环内里不会出来,此时如果你再启动客户端2,那么就会取不到连接,也就是调用不了accept方法.此处客户端处置惩罚processConnection自己就是要长时间执行的,也不知道客户端啥时候竣事,也不知道客户端要发送多少请求,我们期望在执行processConnection方法的同时也快速调用到accept,此时就须要用到多线程.主线程里,专门负责拉客(获取客户端)拉倒客人之后,创建新的线程,让新的线程负责处置惩罚客户端的各种请求.
服务器代码(修改后的)
  1. package TCP;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. import java.io.PrintWriter;
  6. import java.net.ServerSocket;
  7. import java.net.Socket;
  8. import java.util.Scanner;
  9. /**
  10. * Created with IntelliJ IDEA
  11. * Description:
  12. * User: lenovo
  13. * Date: 2024 -01 -24
  14. * Time: 13:49
  15. */
  16. public class TcpEchoSever {
  17.     private ServerSocket serverSocket=null;
  18.     public TcpEchoSever(int port) throws IOException {
  19.         serverSocket=new ServerSocket(port);
  20.     }
  21.     public void start() throws IOException {
  22.         //从内核中的队列当中取任务
  23.         System.out.println("服务器开始工作!");
  24.         while(true){
  25.             Socket clientSocket=serverSocket.accept();
  26.             //处理连接的逻辑
  27.             //此处我们要实现一边拉客一边介绍,那么要实现这个功能就需要多线程编程,主线程负责拉客,创建出来的新的线程负责服务
  28.             Thread t=new Thread(()->{
  29.                 procesConnection(clientSocket);
  30.             });
  31.             t.start();
  32.         }
  33.     }
  34.     //处理连接的逻辑
  35.     private void procesConnection(Socket clientSocket) {
  36.         System.out.printf("[%s:%d]\n",clientSocket.getInetAddress().toString(),clientSocket.getPort());
  37.         try(InputStream inputStream=clientSocket.getInputStream();
  38.             OutputStream outputStream= clientSocket.getOutputStream()){
  39.             while(true){
  40.                 Scanner scanner=new Scanner(inputStream);
  41.                 if(!scanner.hasNext()){
  42.                     System.out.println("服务器关闭!");
  43.                     break;
  44.                 }else{
  45.                     //1.读取数据,只有在传过来的数据是文本状态时才可以这么做
  46.                     //这个代码暗藏一个约定,客户端发过来的请求得是文本数据,同时还得带有空白符作为分割
  47.                     //空白符:空格 换行 回车 制表符 垂直制表符 等等
  48.                     String request=scanner.next();
  49.                     //2.处理请求
  50.                     String rec=processrequest(request);
  51.                     //3.发送数据,由于这里是TCP协议,所以要用字节流来发送,scanner是字符流
  52.                     PrintWriter printWriter=new PrintWriter(outputStream);
  53.                     //这里使用println是为了有空白符(在结尾加上\n,方便客户端使用scanner.next来读取响应).
  54.                     //网络程序讲究的就是客户端和服务器能够配合!
  55.                     printWriter.println(rec);
  56.                     //这个操作是为了刷新缓区.
  57.                     printWriter.flush();
  58.                     //日志,在屏幕上显示一下
  59.                     System.out.printf("[%s;%d] %s %s\n",clientSocket.getInetAddress().toString(),
  60.                             clientSocket.getPort(),request,rec);
  61.                 }
  62.             }
  63.         }catch (IOException e){
  64.             e.printStackTrace();
  65.         }finally{
  66.             try{
  67.                 clientSocket.close();
  68.             }catch (IOException e){
  69.                 e.printStackTrace();
  70.             }
  71.         }
  72.     }
  73.     private String processrequest(String request) {
  74.         return request;
  75.     }
  76.     public static void main(String[] args) throws IOException {
  77.             TcpEchoSever tcpEchoSever=new TcpEchoSever(3060);
  78.             tcpEchoSever.start();
  79.     }
  80. }
复制代码

这样就可以满足要求了.
经过上述改进,只要服务器系统资源充足,有几个客户端都是可以的了.
TCP程序的时候,涉及到两种写法:
1.一个连接中只能传输一次请求与相应.(短连接)
2.一个连接中,可以传输多次请求可相应.(长连接)
那么如今是有一个连接,就有一个新的线程.如果很多客户端,频繁的来连接/断开,服务器就涉及到频繁创建/开释线程了.(所以使用线程池是一个比较好的方案)

固然这里我们使用线程池,避免了频繁创建烧毁线程,但是毕竟是每个客户端对应一个线程,如果服务器对应的客户端很多,服务器就须要创建出大量的线程,对于服务器的开销是很大的.
是否有办法,使用一个线程(或者最多三四个线程)就可以高效的处置惩罚很多的客户端的并发请求(几万个/几十万个客户端)?

C10K:同一时刻有10k个客户端(1w个客户端)通过前面的一些技术本事,硬件设备,是可以处置惩罚好(线程池之类的就可以)
随着互联网的发展,客户端越来越多,请求也越来越多
C10M:同一时刻,有1kw的客户端并发请求(对于一些大厂来说,也不是什么稀奇的事变),对此我们引入了很多技术本事,其中一个非常有效/须要的本事,IO多路复用/IO多路转换(这个东西是解决高并发(C10M)的紧张本事之一,但不是说这招一出,一下就能解决).
解决高并发,说白了就是四个字

1.开源:引入更多的硬件资源.
2.节流:提高单位硬件资源能够处置惩罚的请求数(本质上就是减少线程数目).
也就是同样的请求数,斲丧的硬件资源更少.

关于IO多路复用的API可以去搜刮JavaNIO一些关键词.


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立聪堂德州十三局店

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表