2021.2.20 java 基础网络编程学习

基础网络编程

一.基本概念

1.计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统(https://baike.baidu.com/item/网络操作系统/3997),网络管理软件网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

2.网络编程的目的

  • 让计算机与计算机之间建立连接、通信

  • 传播交流信息

  • 数据交换

  • 通信

3.想达到这个效果需要什么

  • 1)如何准确地定位网络上的一台主机--------192.168.16.124:端口,定位到这个计算机上的某个资源
  • 2)找到这个主机,如何传输数据?

javaweb开发:网页编程 B/S架构(是通过浏览器去访问)

网络编程:TCP/IP 客户端 C/S结构(比如登录qq这些)

  • 服务端
    • 自定义 S
    • Tomcat服务器 S:java后台开发所用服务器
  • 客户端
    • 自定义 C
    • 浏览器 B

二.网络通信的要素

1.如何实现网络的通信

  • 1)需要知道通信双方的地址(ip+端口号)
  • 2)规则:网络通信的协议

TCP/IP参考模型:

两种模型

重点关注传输层的TCP和UDP

三.IP

1.ip协议的基本概念

  • 该协议为网络层协议

  • 负责数据从一台机器发送到另外一台机器

  • 给互联网每台设备分配唯一标识(IP地址

2. ip地址的基本概念

  • 可以唯一定位一台网络上的计算机

  • 127.0.0.1是本机地址

  • ip地址的分类

    • ipv4/ipv6

    ipv4/ipv6区别

    • 公网(互联网)/私网(局域网)

      • ABCD类

      image-20210216215951828

    公网/私网

    • 总结上网流程:

      我们能够上网靠的是isp组织分给我们的Ip地址,但是这个ip地址一般不是给个人的,一般都是给一个单位,一个区域的,也就是说我们实际上能接触到的一般都是私有地址,即我们用ipconig查到的都是私有地址,也就相当于局域网内的ip地址,当我们真正联网时,会先把数据发送到路由,然后再由路由进行处理实现真正的联网操作,路由的地址才是真正联网的Ip地址,也就是pubilc ip,而我们在自己电脑上查到的都是私有ip

  • 域名-----域名的诞生是为了解决记忆IP问题

    • 比如www.baidu.com

四.端口

1.端口的基本概念

  • 端口表示计算机上的一个程序的进程,端口号是在通信实体上进行网络通讯程序的唯一标识

  • 不同的进程有不同的端口号!!!用来区分软件

  • 端口被规定有0~65535个端口

  • 单个协议下面,端口号不能冲突(tcp,udp各有65535个端口 ,不同协议下,端口号可以相同)

  • 端口分类

    • 公有端口0~1023
      • HTTP(Web服务器,访问网站):80
      • HTTPS:443
      • FTP服务器(文件传输):21
      • SMTP(发邮件的):25
      • Talent:23
    • 程序注册端口:1024~49151,用来分配用户或者程序
      • Tomcat:8080
      • MySQL:3306
      • Oracle:1521
  • 动态、私有:49152~65535

    通过Dos控制台查看端口/进程

2.端口的代码

//测试端口
public class demo03 {
    public static void main(String[] args) {
        InetSocketAddress isa1=new InetSocketAddress("127.0.0.1",8080);
        InetSocketAddress isa2=new InetSocketAddress("localhost",8080);
        InetSocketAddress isa3=new InetSocketAddress("www.baidu.com",8080);
        System.out.println(isa1);
        System.out.println(isa2);
        System.out.println(isa3);//得到主机名+ip地址+端口
        System.out.println("==============================================================");
        System.out.println(isa1.getHostName());//得到主机名
        System.out.println(isa3.getHostName());//得到主机名
        System.out.println(isa1.getAddress());//得到ip地址
        System.out.println(isa3.getAddress());//得到ip地址
        System.out.println(isa1.getPort());//得到端口号
        System.out.println(isa3.getPort());//得到端口号
    }
}
/*
/127.0.0.1:8080
localhost/127.0.0.1:8080
www.baidu.com/183.232.231.172:8080
==============================================================
127.0.0.1
www.baidu.com
127.0.0.1/127.0.0.1
www.baidu.com/183.232.231.172
8080
8080

五.通信协议

1.TCP/IP协议簇(实际上是一组协议)

TCP/IP模型

  • 重要的协议
    • TCP:用户传输协议------比如打电话
    • UDP:用户数据报协议-----比如发短信发邮件
  • 出名的协议
    • TCP
    • IP:网络互连协议

2.TCP和UDP的对比

  • UDP协议和TCP协议都是传输层协议。

  • TCP

    • 概念:TCP(Transmission Control Protocol,传输控制协议)提供的是面向连接,可靠的字节流服务。即客户和服务器交换数据前,必须现在双方之间建立一个TCP连接,之后才能传输数据。并且提供超时重发,丢弃重复数据,检验数据,流量控制等功能,保证数据能从一端传到另一端(保证数据的准确性和顺序)。
    • 特点:
      • 1)面向连接
        img

    建立连接的过程需要:TCP三次握手

    1. 主机A通过向主机B 发送一个含有同步序列号标志位的数据段(SYN)给主机B ,向主机B 请求建立连接,通过这个数据段,主机A告诉主机B 两件事:我想要和你通信;你可以用哪个序列号作为起始数据段来回应我。
    2. 主机B 收到主机A的请求后,用一个带有确认应答(ACK)和同步序列号(SYN)标志位的数据段响应主机A,也告诉主机A两件事:我已经收到你的请求了,你可以传输数据了;你要用哪个序列号作为起始数据段来回应我。
    3. 主机A收到这个数据段后,再发送一个确认应答,确认已收到主机B 的数据段:“我已收到回复,我现在要开始传输实际数据了”。

    这样3次握手就完成了,主机A和主机B 就可以传输数据了。

    断开连接的过程需要:TCP四次挥手

    1. 当主机A完成数据传输后,将控制位FIN置1,提出停止TCP连接的请求。
    2. 主机B收到FIN后对其作出响应,确认这一方向上的TCP连接将关闭,将ACK置1。
  1. 由B 端再提出反方向的关闭请求,将FIN置1。
  2. 主机A对主机B的请求进行确认,将ACK置1,双方向的关闭结束。
//A:B 啊,我不想玩了
//B:哦,你不想玩了啊,我知道了
这个时候,只是 A 不想玩了,即不再发送数据,但是 B 可能还有未发送完的数据,所以需要等待 B 也主动关闭。
//B:A 啊,好吧,我也不玩了,拜拜
//A:好的,拜拜
    • 2)可靠传输:

    序列号 确认应答 超时重传 拥塞控制

    • 3)面向字节流:

    创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;
    另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工 。

① 调用write时, 数据会先写入发送缓冲区中;

② 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出; 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;

③ 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;

④ 然后应用程序可以调用read从接收缓冲区拿数据;

  • 4)会出现TCP粘包问题

① 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包;

② 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段;

③ 站在传输层的角度, TCP是一个一个报文过来的,按照序号排好序放在缓冲区中;

④ 站在应用层的角度, 看到的只是一串连续的字节数据. 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分是一个完整的应用层数据包。
  • UDP

    • 概念:UDP(User Data Protocol,用户数据报协议)是一个简单的面向数据报的运输层协议。它不提供可靠性,只是把应用程序传给IP层的数据报发送出去,但是不能保证它们能到达目的地。(可能会丢包,不保证数据的顺序和正确性)由于UDP在传输数据报前不用再客户和服务器之间建立一个连接,且没有超时重发等机制,所以传输速度很快。
    • 特点:
      • 1)无连接:知道对端的IP和端口号就直接进行传输, 不需要建立连接。
      • 2)不可靠:没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息。
      • 3)面向数据报:不能够灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。
      • 4)数据收不够灵活,但是能够明确区分两个数据包,避免粘包问题。
  • TCP和UDP的报头不同

  • TCP和UDP的协议不同

六.InetAddress类

1.InetAddress类的基本概念和方法

InetAddress类

2.InetAddress类的使用

//测试IP
//假设我电脑的ip地址为192.168.31.226,电脑名为:DESKTOP-14J6G9R
public class demo01 {
    public static void main(String[] args) {
        try {
//一.创建本机ip地址对象
        //1.使用getLocalHost()方法
            InetAddress ia1=InetAddress.getLocalHost();
            //通过主机的地址返回ip地址对象
            System.out.println("ip地址:"+ia1.getHostAddress()+"主机名:"+ia1.getHostName());
            System.out.println(ia1);
            System.out.println("======================================");
        //2.使用getByName("ip地址/主机名")方法
            InetAddress ia2=InetAddress.getByName("DESKTOP-14J6G9R");
            //通过主机的名字返回ip地址对象
            InetAddress ia3=InetAddress.getByName("192.168.31.226");
            //通过主机的地址返回ip地址对象
            System.out.println("ip地址:"+ia2.getHostAddress()+"主机名:"+ia2.getHostName());
            System.out.println("ip地址:"+ia3.getHostAddress()+"主机名:"+ia3.getHostName());
            System.out.println(ia2);
            System.out.println(ia3);
            System.out.println("======================================");
        //3.使用getByName("127.0.0.1")方法
            InetAddress ia4=InetAddress.getByName("127.0.0.1");
            //通过(主机的地址/主机名)返回ip地址对象
            System.out.println("ip地址:"+ia4.getHostAddress()+"主机名:"+ia4.getHostName());
            System.out.println(ia4);
            System.out.println("======================================");
        //4.使用getByName("localhost")方法
            InetAddress ia5=InetAddress.getByName("localhost");
            //通过主机名返回ip地址对象
            System.out.println("ip地址:"+ia5.getHostAddress()+"主机名:"+ia5.getHostName());
            System.out.println(ia5);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}
/*
结果:
ip地址:192.168.31.226主机名:DESKTOP-14J6G9R
DESKTOP-14J6G9R/192.168.31.226
======================================
ip地址:192.168.31.226主机名:DESKTOP-14J6G9R
ip地址:192.168.31.226主机名:DESKTOP-14J6G9R
DESKTOP-14J6G9R/192.168.31.226
DESKTOP-14J6G9R/192.168.31.226
======================================
ip地址:127.0.0.1主机名:127.0.0.1
127.0.0.1/127.0.0.1
======================================
ip地址:127.0.0.1主机名:localhost
localhost/127.0.0.1
*/

注意:

1. InetAddress(是没有构造器的,只能通过它的静态方法返回这个对象)

2.如果利用主机地址构造ip地址对象,则打印时,是不知道主机名的,只有找到了主机名,才能显示出来

 InetAddress ia3=InetAddress.getByName("192.168.31.226");
 //通过主机的地址返回ip地址对象
 System.out.println("ip地址:"+ia3.getHostAddress()+"主机名:"+ia3.getHostName());
 System.out.println(ia3);
/*
结果:
ip地址:192.168.31.226主机名:DESKTOP-14J6G9R
DESKTOP-14J6G9R/192.168.31.226
*/



//若这两句互换位置
System.out.println(ia3);
System.out.println("ip地址:"+ia3.getHostAddress()+"主机名:"+ia3.getHostName());
/*
结果:
/192.168.31.226
ip地址:192.168.31.226主机名:DESKTOP-14J6G9R
*/
//测试IP2
//假设我电脑的ip地址为192.168.31.226
public class demo02 {
    public static void main(String[] args) {
        try {
//二.创建局域网ip地址对象
           InetAddress ia6=InetAddress.getByName("192.168.101.48");
            System.out.println("ip地址:"+ia6.getHostAddress()+"主机名:"+ia6.getHostName());
            System.out.println("2s钟是否可以找到该地址的对象:"+ia6.isReachable(2000));
            //结果为false,因为这个地址是不存在的,没有主机
//三.创建外网ip地址对象
            InetAddress ia7=InetAddress.getByName("www.baidu.com");
            //通过域名返回ip地址对象
            System.out.println("ip地址:"+ia7.getHostAddress()+"主机名:"+ia7.getHostName());
            System.out.println("2s钟是否可以找到该地址的对象:"+ia7.isReachable(2000));
            //结果为true,因为这个地址是存在的
            System.out.println("=========================================");
            System.out.println("打印“www.baidu.com”这个域名所有ip地址对象:");
            InetAddress[] inetaddresses=InetAddress.getAllByName("www.baidu.com");
            for(InetAddress ia:inetaddresses){
                System.out.println(ia.getHostAddress());
            }
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
/*
ip地址:192.168.101.48主机名:192.168.101.48
2s钟是否可以找到该地址的对象:false
ip地址:183.232.231.174主机名:www.baidu.com
2s钟是否可以找到该地址的对象:true
=========================================
打印“www.baidu.com”这个域名所有ip地址对象:
183.232.231.174
183.232.231.172

注意:一个域名是可以有多个ip地址对象的

七.基于TCP网络编程

1.Socket编程

  • Socket(套接字)是网络中的一个通信节点
  • 分为客户端Socket与服务器ServerSocket
  • 通信要求是:IP地址+端口号
  • 开发步骤

完整的抛出异常写法

2.基于TCP协议的服务器端开发(ServerSocket类)

  • ServerSocket类实现服务器套接字。服务器套接字等待请求通过网络传入,它基于该请求执行某些操作,然后可能向请求者返回结果

3.基于TCP协议的客户端开发(Socket类)

3.1 案例1:TCP编程实现客户端发送数据给服务器端

//基于TCP协议的服务器开发
public class TcpServer1 {
    public static void main(String[] args)throws Exception {
//1.创建ServerSocket(服务器套接字),并指定端口号
        ServerSocket serverSocket=new ServerSocket(9876);
//2.调用accept()方法,接收客户端请求,这是一个阻塞方法(如果没有客户端请求,它就会发生堵塞)
        System.out.println("服务器已启动");
        Socket socket=serverSocket.accept();//该方法返回的是客户端Socket套接字
//3.获取输入流,读取客户端发送请求的数据
        InputStream is=socket.getInputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedReader br=new BufferedReader(new InputStreamReader(is,"utf-8"));
        String date=br.readLine();
        System.out.println("客户发送的数据为:"+date);
//4.获取输出流,发送数据给客户端
//5.关闭释放资源
        br.close();
        socket.close();
        serverSocket.close();
    }
}
//基于TCP协议的客户端开发
public class TcpClient1 {
    public static void main(String[] args)throws Exception {
//1.创建Socket(客户端套接字),并指定服务器的地址和端口号
        Socket socket=new Socket(InetAddress.getByName("127.0.0.1"),9876);
//2.获取输出流,发送请求数据给服务器
        OutputStream os=socket.getOutputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
        bw.write("我哭了很累!");
//3.获取输入流,读取服务器回复的数据
//4.关闭释放资源
        bw.close();
        socket.close();
     }
}
先运行服务端,再运行客户端

在服务器端
结果:
服务器已启动
客户发送的数据为:我哭了很累!

3.2 案例2:TCP编程实现客户端上传文件给服务器端

public class TcpFileServer1 {
    public static void main(String[] args) throws Exception{
        //1.创建服务器套接字
        ServerSocket serverSocket=new ServerSocket(9999);
        System.out.println("服务器已启动。。。");
        //2.利用accept()方法,接受客户端的请求
        Socket socket=serverSocket.accept();
        //3.接收客户端的请求数据,创建输入流
        InputStream is=socket.getInputStream();
        //4.接收客户端的请求的文件,边读边保存
        FileOutputStream fos=new FileOutputStream("D:\\ajava\\3.jpg");
        int count=0;
        byte[] buf=new byte[1024*4];
        while ((count=is.read(buf))!=-1){
             fos.write(buf,0,count);
        }
         //5.从服务端发送响应到客户端,已经传输完成,创建输出流
        OutputStream os=socket.getOutputStream();
        os.write("服务器接收文件完成".getBytes());
        //6.关闭
        fos.close();
        socket.close();
        serverSocket.close();
        System.out.println("服务器接收完毕");
    }
}

public class TcpFileClient1 {
    public static void main(String[] args) throws Exception {
        //1.创建客户端套接字
        Socket socket =new Socket(InetAddress.getByName("127.0.0.1"),9999);
        //2.发送请求数据,创建输出流
        OutputStream os=socket.getOutputStream();
        //3.发送客户端文件,边读取文件,边发送
        FileInputStream fis=new FileInputStream("D:\\ajava\\1.jpg");
        byte[] buf=new byte[1024*4];
        int count =0;
        while((count=fis.read(buf))!=-1){
            os.write(buf,0,count);
        }
        //通知服务器,我已经发送完了
        socket.shutdownOutput();

        //5.接收服务器传回接收完成的响应,创建输入流
        InputStream is=socket.getInputStream();
        BufferedReader br=new BufferedReader(new InputStreamReader(is,"utf-8"));
        String date=br.readLine();
        System.out.println(date);
        //6.关闭
        fis.close();
        os.close();
        socket.close();
        System.out.println("客户端发送完毕");
    }

}

先运行服务端,再运行客户端

在客户端
结果:
服务器接收文件完成
客户端发送完毕

在服务器端
结果:
服务器已启动。。。
服务器接收完毕
    
在D://ajava中成功发送了一个和1.jpg相同的3.jpg


3.3 案例3: TCP实现多个客户端发送数据给服务器端

  • 服务端先创建一个服务器套接字,利用accept()方法接收客户端的socket,对于接收到的socket,利用线程对象,接收数据
public class TcpServerXlisten {
    public static void main(String[] args)throws Exception {
 //创建一个线程池来管理客户端请求的线程
        Executor ec=Executors.newCachedThreadPool();
//1.创建ServerSocket(服务器套接字),并指定端口号
        ServerSocket listen=new ServerSocket(1234);
        System.out.println("服务器已启动,开始监听");
//2.调用accept()方法,接收客户端请求,这是一个阻塞方法(如果没有客户端请求,它就会发生堵塞)
        //while循环实习持续的接收客户端请求
        while(true) {
            Socket socket = listen.accept();//接收客户端的请求
        // 利用上面创建的线程池来执行---这个线程,负责接收数据
           ec.execute(new SocketThread(socket));
        }
    }
}

  • 创建一个SocketThread的socket线程类,重写run()方法
public class SocketThread extends Thread{
    private Socket socket;
    BufferedReader br=null;
    BufferedWriter bw=null;
    public SocketThread(Socket socket){
        this.socket=socket;
    }
    @Override
    public void run() {
        //3.如果接收到了Socket
            try {
                System.out.println(socket.getInetAddress()+"已经进入!!!!");
                //3.1 获取输入流,读取客户端发送的请求数据
                InputStream is = socket.getInputStream();
                //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
                br = new BufferedReader(new InputStreamReader(is, "utf-8"));
                //3.2 从服务端发送响应到客户端,已经传输完成,创建输出流
                OutputStream os=socket.getOutputStream();
                bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));

                while(true) {
              //在服务端接收客户端发送的数据      
                    String date = br.readLine();
                    System.out.println(socket.getInetAddress()+"发送了:" + date);
              //从服务端发送响应给客户端,告诉其发送成功了!
                    bw.write("你已经发送成功:"+date);
                    bw.newLine();
                    bw.flush();
                    if(date.equals("byebye") || date.equals("886")){
                        break;
                    }
                }
            }catch (IOException e){
                e.printStackTrace();
            }finally {
        //4.释放资源
                try {
                    br.close();
                    bw.close();
                    socket.close();
                    System.out.println(socket.getInetAddress()+"已经退出!!!!");
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
    }
}

  • **注意: 子类不能抛出比父类更多的异常(包括范围更大的异常),因为在Runnable父类中没有抛出异常,所以在实现Runnable时不能抛出异常,对于此处的run()方法抛出异常只能try。 **
  • 写多个客户端,给服务端发送数据
public class TcpClientX1 {
    public static void main(String[] args)throws Exception {
//1.创建Socket(客户端套接字),并指定服务器的地址和端口号
        Socket socket=new Socket(InetAddress.getByName("127.0.0.1"),1234);
//2.获取输出流,发送请求数据给服务器
        OutputStream os=socket.getOutputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
//3. 获取输入流,获得从服务器端的响应信息
        InputStream is = socket.getInputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
//控制输入,将输入的数据----利用字符输出流传给服务端
        Scanner input =new Scanner(System.in);
        while(true) {
            String date=input.nextLine();
            bw.write(date);
            bw.newLine();
            bw.flush();
      //客户端接收服务端传回的发送成功响应!!!     
            String back_date=br.readLine();
            System.out.println(back_date);
            if(date.equals("886")||date.equals("byebye")){
                break;
            }
        }
//4.关闭释放资源
        br.close();
        bw.close();
        socket.close();
    }
}


public class TcpClientX2 {
    public static void main(String[] args)throws Exception {
//1.创建Socket(客户端套接字),并指定服务器的地址和端口号
        Socket socket=new Socket(InetAddress.getByName("DESKTOP-14J6G9R"),1234);
//2.获取输出流,发送请求数据给服务器
        OutputStream os=socket.getOutputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(os,"utf-8"));
//3. 获取输入流,获得从服务器端的响应信息
        InputStream is = socket.getInputStream();
        //将字节流转换为字符流,这样可以传输中文,再将其功能进行加强,变为字符缓冲输入流
        BufferedReader br = new BufferedReader(new InputStreamReader(is, "utf-8"));
//控制输入,将输入的数据----利用字符输出流传给服务端
        Scanner input =new Scanner(System.in);
        while(true) {
            String date=input.nextLine();
            bw.write(date);
            bw.newLine();
            bw.flush();
    //客户端接收服务端传回的发送成功响应!!!         
            String back_date=br.readLine();
            System.out.println(back_date);
            if(date.equals("886")||date.equals("byebye")){
                break;
            }
        }
//4.关闭释放资源
        br.close();
        bw.close();
        socket.close();
    }
}

先启动服务端,再启动上面两个客户端,在客户端的控制台上输入:可以见到如下结果:

TcpClientX1:
你好我是X1
你已经发送成功:你好我是X1
886
你已经发送成功:886
TcpClientX2:
你好我是X2
你已经发送成功:你好我是X2
byebye
你已经发送成功:byebye
TcpServerListen:
服务器已启动,开始监听
/127.0.0.1已经进入!!!!
/192.168.31.226已经进入!!!!
/127.0.0.1发送了:你好我是X1
/192.168.31.226发送了:你好我是X2
/127.0.0.1发送了:886
/127.0.0.1已经退出!!!!
/192.168.31.226发送了:byebye
/192.168.31.226已经退出!!!!

3.4 案例4:TCP编程实现多个客户端通信

思路如下:

  • 写一个服务端Servce,和案例3:Tcp编程实现客户端发送数据给服务端一样,服务端创建一个线程池管理线程,它需要持续的监听,有一个ServeeSocket,当有客户端的Socket与其(ServerSocket)相同时,就会连接,服务端会开辟一个线程(UserSocket类继承了Thread类或者Runnable接口)并启动它,增加的内容是,服务端需要创建一个线程集合(Vector),存储这些线程,因为客户端进行通信,是通过这些线程传递的。注意:每次开辟一个线程,都要将其加入到线程集合之中

  • 对于每一个线程UserSocket,利用线程对象接收发送数据,发送的内容为Message类(里面包括,传送者from,接收者头,消息内容Info,消息类型MessageType,),所以要用到的是对象流ObjectOutputStream和ObjectInputStream

    • readObject()--------从流中读取一个对象(从流中读取一个对象,也就是从文件中读取一个对象,称为反序列化

    ​ writeObject()------- --向流中写入一个对象(将内存中的对象写入流中,也就是写入文件中,称为序列化

  • 对于UserSocket类,它需要通过对象输入流,接收来自客户端发送的信息,需要先读取全部的消息readObject(),再查看它的消息类型,进行不同操作,比如可以有的消息类型:登陆和发送

    • 登陆的话,就需要获得该消息的from(也就是发送者),System.out.print给服务端,说明其已经连接进来服务端了,然后利用对象输出流,给当前这个线程(this),返还该消息(this.oss.writeObject()),相当于给可客户端一个已接收到消息的响应!
    • 发送的话,就需要获得该消息的to(也就是接收者),在线程集合Vector中是否有与该to(接收者)同名的线程名(to.equal(user.name)),如果有则利用对象输出流向这个同名的线程user,发送消息(user.oss.writeObject())
  • 多客户端进行通信,使用服务器作为中转消息站,这是把消息进行集中处理

    • 编写客户端Client,同样也是利用ObjectOutputStream和ObjectInputStream对象输出输入流来传递信息,刚开始会进行登录操作,利用对象输出流,传递的信息只有from发送者,和消息类型(登录),这样才能让服务端保留这个客户端的线程,方便后续操作
    • 当消息类型是发送时,就发送信息(包括,from、to、info、MessageType)
    • 最为重要的是,每个客户线程(单线程)需要接收其他线程发来的数据,利用对象输入流,先读取全部的信息readObject(),再进行整理输出在客户端的控制台上
  • 源码如下:

//Messagel类
public class Message implements Serializable {
    private String info;
    private String from;
    private String to;
    private int type;
    public Message() {
    }
    public Message(String info, String from, String to, int type) {
        this.info = info;
        this.from = from;
        this.to = to;
        this.type = type;
    }
    public String getInfo() {
        return info;
    }
    public void setInfo(String info) {
        this.info = info;
    }
    public String getFrom() {
        return from;
    }
    public void setFrom(String from) {
        this.from = from;
    }
    public String getTo() {
        return to;
    }
    public void setTo(String to) {
        this.to = to;
    }
    public int getType() {
        return type;
    }
    public void setType(int type) {
        this.type = type;
    }
}
//MessageType类
public final class MessageType {
    public static final int Type_Login=0;
    public static final int  Type_Send=1;
}

//服务端
public class Server {
    public static void main(String[] args) throws Exception{
        //服务端创建一个线程池来管理客户端请求的线程
        Executor ec=Executors.newFixedThreadPool(3);
        //服务端保存客户端请求的线程---利用集合
        Vector<UserThread> vector=new Vector<>();
//1.创建ServerSocket(服务器套接字),并指定端口号
        ServerSocket listen=new ServerSocket(1000);
        System.out.println("服务器已启动,开始监听");
//2.调用accept()方法,接收客户端请求,这是一个阻塞方法(如果没有客户端请求,它就会发生堵塞)
        //while循环实现持续的接收客户端请求
        while(true) {
            Socket socket = listen.accept();//接收客户端的请求
            // 利用上面创建的线程池来执行---这个线程,负责接收数据
            ec.execute(new UserThread(socket,vector));
        }
    }
}
//服务端用来处理客户端线程的类
class UserThread extends Thread {
    private String name;
    private Socket socket;
    private Vector<UserThread> vector;//客户端请求线程的集合
    ObjectOutputStream oos;
    ObjectInputStream ois;
    public UserThread(Socket socket, Vector<UserThread> vector) {
        this.socket = socket;
        this.vector = vector;
        vector.add(this);
    }
    @Override
    public void run() {
        try {
            //System.out.println(socket.getInetAddress() + "已经进入!!!!");
            //3.1 获取输入流,读取客户端发送的请求数据
            InputStream is = socket.getInputStream();
            ois = new ObjectInputStream(is);
            //3.2 从服务端发送响应到客户端,已经传输完成,创建输出流
            OutputStream os = socket.getOutputStream();
            oos = new ObjectOutputStream(os);
            //实现持续通信
            while(true){
                //服务端利用对象输入流,读取客户端发送的消息对象
                Message msg = (Message) ois.readObject();
                //判断消息的类型
                int type =msg.getType();
                switch (type){
                    case MessageType.Type_Login:
                        name=msg.getFrom();
                        System.out.println(name+"进入服务器!!");
                        oos.writeObject(msg);
                        break;
                    case MessageType.Type_Send:
                        String to=msg.getTo();
                        String info=msg.getInfo();
                        for (UserThread user:vector) {
                            if (to.equals(user.name) && user != this) {
                                    user.oos.writeObject(msg);
                            break;
                            }
                        }
                        break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            try {
                ois.close();
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
}

//客户端,可以写多个!!!
public class Client {
    public static void main(String[] args)throws Exception {
        Scanner input =new Scanner(System.in);
        Executor ec= Executors.newSingleThreadExecutor();
//1.创建Socket(客户端套接字),并指定服务器的地址和端口号
        Socket socket=new Socket("localhost",1000);
        //2.1 从客户端发送数据到服务端,创建输出流
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
//控制输入,将输入的数据----利用字符输出流传给服务端
            System.out.println("请输入你的名字:");
            String name = input.nextLine();
            Message msg = new Message(null, name, null, MessageType.Type_Login);
            oos.writeObject(msg);
            //2.2 获取输入流,读取服务端响应的数据
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            msg = (Message) ois.readObject();
            System.out.println(msg.getFrom() + "登录成功!!");
            //3.启动单线程,用来接收其他线程发送的消息
            ec.execute(new ReadThread(ois));
            //4.利用输出流,发送数据给服务端
            while (true) {
                Message message = new Message();
                System.out.println("To:");
                message.setTo(input.nextLine());
                System.out.println("Information:");
                String info = input.nextLine();
                message.setInfo(info);
                message.setFrom(name);
                message.setType(1);
                oos.writeObject(message);
            }
    }
}
public class Client2 {
    public static void main(String[] args)throws Exception {
        Scanner input =new Scanner(System.in);
        Executor ec= Executors.newSingleThreadExecutor();
//1.创建Socket(客户端套接字),并指定服务器的地址和端口号
        Socket socket=new Socket(InetAddress.getByName("DESKTOP-14J6G9R"),1000);
        。。。后面一样
            
//客户端用来接收其他线程发送消息的线程类
class ReadThread extends Thread{
    ObjectInputStream ois;
    public ReadThread(ObjectInputStream ois) {
        this.ois = ois;
    }
    @Override
    public void run() {
        try {
            while(true) {
                //利用输入流,读取服务端发送的数据
                Message msg = (Message) ois.readObject();
                System.out.println(msg.getFrom() + " say to me: " + msg.getInfo());
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }finally {
            try {
                ois.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}            

Server:                   
服务器已启动,开始监听
lyj进入服务器!!
zyx进入服务器!!
    
Client:
请输入你的名字:
lyj
lyj登录成功!!
To:
       zyx say to me: I love you!
zyx
Information:
I love you too!
To:

Client2:
请输入你的名字:
zyx
zyx登录成功!!
To:
lyj
Information:
I love you!
To:
      lyj say to me: I love you too!

4.基于UDP协议的开发

4.基于UDP协议的开发

  • 不用进行连接(服务器),只是需要对方地址

  • 没有 客户端和服务端的说法,即可以作为接收端,也可以作为发送端

  • 案例1:基于UDP协议的发送端发送信息,接收端接收信息

//Udp:不需要建立服务器连接
public class UdpSent1 {
    public static void main(String[] args) throws Exception{
    //1.建立一个Soceket
        DatagramSocket socket=new DatagramSocket();
    //2.建一个包用来发送数据
        String s="你好!!";
        //发送给谁
        InetAddress ia1=InetAddress.getByName("localhost");
        int port=9990;
        //数据,数据的起始位置,数据的末位置,发送的ip地址,发送的端口号
        DatagramPacket packet=new DatagramPacket(s.getBytes(),0,s.getBytes().length,ia1,port);
        //DatagramPacket(数组)
    //3.发送包
        socket.send(packet);
    //4.关闭
        socket.close();
    }
}

//UDP:等待发送端发来的消息
public class UdpRec1 {
    public static void main(String[] args) throws Exception {
    //1.创建Socket,来开放发送端要发到的端口
        DatagramSocket socket=new DatagramSocket(9990);
    //2.新建一个包用来接收数据
        byte[] buf=new byte[1024];
        DatagramPacket packet=new DatagramPacket(buf,0,buf.length);
        //接收
    //3.接收包
        socket.receive(packet);//阻塞接收
        System.out.println(packet);
        System.out.println("发送包的地址为::"+packet.getAddress());
        System.out.println("发送包的内容为:"+new String(packet.getData()));
        //packet.getData()得到的数据为数组,要转为字符串输出

    //4.关闭
        socket.close();
    }
}

先运行发送端,再运行接收端

接收端
结果:
发送包的地址为::/127.0.0.1
发送包的内容为:你好!!                                         

  • 案例2:UDP实现联系发送
//UDP实现循环发送
public class UdpChat1 {
    public static void main(String[] args) throws Exception{
    //1.创建Socket
        DatagramSocket socket=new DatagramSocket(5555);
    //2.创建一个包,用来发送
        //首先先准备数据:利用控制台读取System.in
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        //如果有读取的数据,则一直发送
        while(true) {
            String s = br.readLine();
            byte[] datas = s.getBytes();
            //创建一个包,写好发送的内容,发送的地址
            InetAddress ia = InetAddress.getByName("localhost");
            int port = 6666;
            DatagramPacket packet = new DatagramPacket(datas, 0, datas.length, ia, port);
            //发送包
            socket.send(packet);
            //如果发送的数据为空,则停止发送
            if(s==null){
                break;
            }
        }
    //3.关闭
           br.close();
           socket.close();
    }
}

//UDP实现循环接收
public class UdpChat2 {
    public static void main(String[] args) throws Exception {
        //1.创建Socket,来开放发送端需要发送的端口
            DatagramSocket socket = new DatagramSocket(6666);
         //2.创建一个包,用来接收
        while (true) {
            //首先先准备区域,用来接收发送端的数据
            byte[] buf = new byte[1024];
            //创建一个包,用来接收发送端的包
            DatagramPacket packet = new DatagramPacket(buf, 0, buf.length);
            //接收包
            socket.receive(packet);//阻塞接收

            //断开连接
            byte[] data = packet.getData();
            String stringData =new String(data,0,data.length);
            System.out.println(stringData);
            if (stringData== null) {
                break;
            }
        }
        socket.close();
    }
}

先运行发送端,再运行接收端
    
在发送端的控制台写入:
nihao!hhh
nihao2!
你好
在接收端的控制台出现:
nihao!hhh
nihao2!
你好

posted @ 2021-02-27 18:08  维他命D片  阅读(118)  评论(0编辑  收藏  举报