学Java第五十六天——网络编程

一、什么是网络编程

客户端和浏览器只是用来展示的,真正的核心逻辑都在服务器中。

BS架构

游戏的图片、特效背景音乐都不在电脑本地,而是在服务器中,通过网络传递所有的图片音频资源。电脑玩的时候画质就不好。

CS架构

图片、音频资源在安装的时候都放在电脑本地了,在玩游戏的时候,服务器只需要告诉客户端现在该显示哪张图片了即可。

二、网络编程三要素

2.1IP

2.2 端口号

2.3 协议

三、IP 详细介绍

2.1 IPv4

2.2 IPv6

2.3 IPv4的公网和局域网

网吧里的电脑共享一个公网IP,由路由器为网吧里的每一个电脑分配一个局域网IP。

2.3.1 特殊IP地址

不一样。192.168.1.100是要经过路由器的,而127.0.0.1不经过路由器,网卡看见后直接返回给本机。并且,设备在一个地方和另一个地方被分配的局域网地址会不一样,比如在宿舍和教室的就不一样,所以答案是不一样。

2.3.2 常见CMD命令

ping既可以连通自己所在局域网的其他电脑,也可以连通外网的电脑。

四、InetAddress类

  1. 该类的对象就是一个IP地址对象,其实也就是一个电脑的对象。创建该对象后,就可以向该电脑发送信息。
  2. 该类没有构造方法,是通过getByName静态方法来实现的。

代码举例:

java 复制代码
package com.InetAddress.exe;

import java.net.InetAddress;
import java.net.UnknownHostException;

public class exe01 {
    public static void main(String[] args) throws UnknownHostException {
        InetAddress address = InetAddress.getByName("明的猎人book");
        System.out.println(address);

        String hostAddress = address.getHostAddress();
        System.out.println(hostAddress);

        String hostName = address.getHostName();
        System.out.println(hostName);
    }
}

输出结果

java 复制代码
明的猎人book/192.168.4.1
192.168.4.1
明的猎人book

五、端口号

每个程序都会有一个端口,这是它向其他电脑传输信息的出口或接收其他电脑传来信息的入口。

如果一个程序没有端口对应,那这个程序就是单机的,不联网的。

六、协议

**UDP:**速度快,适用于 发送视频、语音聊天等丢失一点数据不影响整个数据的场景。

TCP: 数据安全,没有丢失,适用于发送文字聊天,发送邮件等如果缺少一点数据就会有影响的场景。

6.1 UDP通信程序

6.1.1 发送数据

DatagramPacket的构造方法的参数:有要发送的内容的字节数组、起始索引、长度、目标电脑的inetAddress对象、目标电脑程序的端口

UDP:对于发送的数据包,接收方能接到就接到,接不到就算了。

java 复制代码
package com.InetAddress.exe;

import java.io.IOException;
import java.net.*;

public class UDP_sent {
    public static void main(String[] args) throws IOException {
        DatagramSocket ds=new DatagramSocket();
        //空参时随机绑定一个端口,以后就用这个端口进行发送消息
        //有参:指定端口号进行绑定。

        String str="我很好,你最近怎么样?";
        byte[] bytes = str.getBytes();
        InetAddress address=InetAddress.getByName("127.0.0.1");
        int port=10086;

        DatagramPacket dp=new DatagramPacket(bytes,bytes.length,address,port);

        ds.send(dp);

        ds.close();


    }
}

代码解析

6.1.2 接收数据

java 复制代码
package com.InetAddress.exe;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;

public class UDP_receive {
    public static void main(String[] args) throws IOException {

        //在接收的时候,一定要先绑定端口。
        //1.先指定接收的快递公司---端口---需要与发送方的包指定的端口一样10086
        DatagramSocket ds=new DatagramSocket(10086);

        //2.接收包裹,但是要先创建一个空字节数组的包裹来 装 送过来的包裹。
        byte[] bytes = new byte[1024];
        DatagramPacket dp=new DatagramPacket(bytes,bytes.length);

        ds.receive(dp);

        //此时接收的包裹把发送内容放到了bytes中

        //3.解析数据包----收到包裹盖打开看看了
        byte[] data = dp.getData();//得到发送数据内容的字节数组,其实就是上面定义的bytes数组
        int len = dp.getLength();//得到发送的内容长度
        InetAddress address = dp.getAddress();//得到发送方地址
        int port = dp.getPort();//得到发送方的发送端口
        String str=new String(bytes,0,len);//将发送方字节内容转为字符串
        System.out.println("接收到数据"+": "+str);
        System.out.println("该数据是从这台电脑"+address+"的这个端口"+port+"发送过来的");

        //4.释放资源
        ds.close();

    }
}

输出结果:要先运行接收的代码,再运行发送的代码

java 复制代码
接收到数据: 我很好,你最近怎么样?
该数据是从这台电脑/127.0.0.1的这个端口55742发送过来的

PS:ds.receive(dp)该代码:在未遇到发送数据时,会阻塞自己,直到发送数据来了将其唤醒,进而接收到数据。

6.1.3 练习

发送代码
java 复制代码
package com.InetAddress.exe;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;

public class UDP_send_test {
    public static void main(String[] args) throws IOException {

        //先快递公司ds指明发送端口
        DatagramSocket ds=new DatagramSocket();//随机端口发送

        //装包裹dp
        Scanner sc=new Scanner(System.in);
        while (true) {
            System.out.println("请输入您要说的话:");
            String s = sc.nextLine();
            if("886".equals(s)){
                break;
            }
            byte[] bytes = s.getBytes();
            InetAddress address = InetAddress.getByName("127.0.0.1");//指明接收地址
            int port=10086;//指明接收的端口
            DatagramPacket dp=new DatagramPacket(bytes,bytes.length,address,port);

            //发送包裹
            ds.send(dp);
        }

        //释放资源
        ds.close();
    }
}
接收代码
java 复制代码
package com.InetAddress.exe;

import cn.hutool.http.body.BytesBody;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDP_receive_test {
    public static void main(String[] args) throws IOException {

        //先指明去哪个快递公司ds---端口号---接收端口号---一定要和发送包dp指定的的端口号一样
        DatagramSocket ds=new DatagramSocket(10086);

        //接收包裹dp,但是要先创建一个dp
        byte[] bytes=new byte[1024];  //用于接收的数组接收一次后,再次接收会把原来的数组内容给覆盖,不会出现乱串。
        DatagramPacket dp=new DatagramPacket(bytes,bytes.length);
        while (true) {
            ds.receive(dp);

            //解析包裹,拿到包裹的发送地址,发送内容,发送内容长度,发送端口
            byte[] data = dp.getData();
            int len = dp.getLength();
            InetAddress address = dp.getAddress();
            int port = dp.getPort();

            System.out.println("ip为"+address+"的电脑发送过来一条消息:"+new String(data,0,len));
        }

    }
}

输出结果

java 复制代码
ip为/127.0.0.1的电脑发送过来一条消息:你好,我叫小智,你叫什么名字?
ip为/127.0.0.1的电脑发送过来一条消息:哈哈哈
ip为/127.0.0.1的电脑发送过来一条消息:哈哈哈,我叫阿呆

还可以让一个代码运行多次:操作如下:

点击"允许多个实例",就可以实现:好多个send控制台

6.1.4 UDP的三种通信方式

单播:一对一

之前的代码都是单播,都是一对一

组播:一对一组
广播:一对局域网全部
组播代码实现

发送

接收:

可以设置多个实例来实现多播,也可以把其中一个接收的添加到224.0.0.2中,会发现,发送不到这个电脑。

广播代码:和单播一模一样,只是把发送包的地址ip对象写成255.255.255.255即可

6.2 TCP通信程序

6.2.1 发送数据

  1. socket对象可以当成通道对象,在这个通道对象中调用outputstream和inputstream来输入输出数据。
  2. socket对象在创建的时候,参数中要指明**"目的地IP","目的地端口"**
  3. serverSocket对象在创建的时候,参数要指明**"端口"与上面的目的地端口****一样。**

客户端代码:

java 复制代码
package com.InetAddress.exe.TCP;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;

public class TCP_client {
    public static void main(String[] args) throws IOException {
        //1.创建socket对象,指明目的地IP和目的地端口
        Socket s=new Socket("127.0.0.1",10086);

        //2.可以从连接通道中获取输出流。
        OutputStream os = s.getOutputStream();
        //写出数据
        os.write("aaa".getBytes());

        //释放资源
        os.close();
        s.close();

    }
}

服务器端代码:

java 复制代码
package com.InetAddress.exe.TCP;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TCP_server {
    public static void main(String[] args) throws IOException {
        //创建serversocket对象,参数端口是接收端口,一定要与client的socket对象的端口一致。
        ServerSocket ss=new ServerSocket(10086);

        //监听客户端的连接
        Socket socket = ss.accept();

        //从连接通道中获取输入流读取数据
        InputStream is = socket.getInputStream();

        //读取数据
        int b;
        while ((b=is.read())!=-1){
            System.out.println((char)b);
        }

        //释放资源
        is.close();
        ss.close();
    }
}

先运行server,再运行client

输出结果:

但是如果输入的是中文就会打印出来是乱码:

中文乱码问题

字节流是一个字节一个字节的写入读取,中文是一个汉字由三个字节组成,所以不能用字节流进行读取,要用字符流进行读取,因为字符流是按编码方式utf-8三个字节进行读取,可以组成一个正确汉字。

所以要用到转换流,把字节流转换成字符流。

client不用变,server代码如下:

java 复制代码
package com.InetAddress.exe.TCP;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class TCP_server2 {
    public static void main(String[] args) throws IOException {
        //创建serversocket对象,参数端口是接收端口,一定要与client的socket对象的端口一致。
        ServerSocket ss=new ServerSocket(10086);

        //监听客户端的连接
        Socket socket = ss.accept();

        //从连接通道中获取输入流读取数据
        InputStream is = socket.getInputStream();

        InputStreamReader isr=new InputStreamReader(is);
        //读取数据
        int b;
        while ((b=isr.read())!=-1){
            System.out.println((char)b);
        }

        //释放资源
        is.close();
        ss.close();

    }
}

输出结果:

代码一些细节

  1. 在client中创建socket对象成功的时候,就已经在client和server中间建立了一个连接通道了,在这个过程中,有一个三次握手底层协议
  2. 输入流和输出流都是在socket这个连接通道里创建的,所以只要关闭这个通道,流就自然会关闭了。在关闭的时候也有一个底层协议:四次挥手,利用这个协议断开连接。
  3. 只有当客户端执行了socket.close()语句,才会关闭连接通道,不然可以一直向服务器发送数据。

多了一个服务器处理最后数据的过程,所以是四次。

HTTP协议底层是TCP。

TCP、UDP发送数据包的底层协议是IP,而TCP三次握手,四次挥手的底层协议是ICMP。

七、综合练习

7.1 发送多次消息

client:

java 复制代码
package com.InetAddress.exe.综合练习.test1;

import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.util.Scanner;

public class client {
    public static void main(String[] args) throws IOException {

        Socket s=new Socket("127.0.0.1",10086);

        OutputStream os = s.getOutputStream();
        Scanner sc=new Scanner(System.in);

        while (true) {
            System.out.println("请输入您要发送的信息:");
            String str = sc.nextLine();
            if("886".equals(str)){
                break;
            }
            os.write(str.getBytes());
        }

        s.close();


    }
}

server

java 复制代码
package com.InetAddress.exe.综合练习.test1;

import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    public static void main(String[] args) throws IOException {

        ServerSocket ss=new ServerSocket(10086);

        Socket socket = ss.accept();

        InputStreamReader isr=new InputStreamReader(socket.getInputStream());

        int b;
        while ((b=isr.read())!=-1){

            System.out.print((char)b);

        }

        socket.close();
        ss.close();

    }
}
  1. 因为只有执行客户端的socket.close()语句,才会关闭连接通道,否则,就可以从客户端一直向服务器发送数据。并且,server不用写循环,就可以不断读取数据,打印数据。
  2. 只要socket没有关闭,server的读取流就可以一直读取流中的数据,不论流中的数据有没有读取完。
  3. 这里和文件的读取写出不一样,当文件中的数据读取完成后,再读取就会读到-1,而在读取客户端就不会读到-1,因为客户端有可能还会再发送信息,所以读不到-1.
  4. 客户端--服务器的read停止机制是:要在发送方发送完数据处写一个结束标记。见7.2题

运行结果

7.2 client-server互相发送消息

  1. 写出流要有最后的结束标记,不然读取流会一直处于要读取的状态。
  2. 双向交流是要在client和server分别建立输出流输入流

client

java 复制代码
package com.InetAddress.exe.综合练习.test2;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;

public class client {
    public static void main(String[] args) throws IOException {
        Socket socket=new Socket("127.0.0.1",10086);

        OutputStream os = socket.getOutputStream();
        String str="我今天很高兴";
        os.write(str.getBytes());

        //写一个写出的结束标记,让服务器的read不要一直等待。
        socket.shutdownOutput();

        //接收server的返回信息并打印
        InputStream is = socket.getInputStream();
        InputStreamReader isr=new InputStreamReader(is);
        int b;
        while ((b=isr.read())!=-1){
            System.out.print((char) b);
        }


        socket.close();


    }
}

server

java 复制代码
package com.InetAddress.exe.综合练习.test2;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    public static void main(String[] args) throws IOException {

        ServerSocket ss=new ServerSocket(10086);

        Socket socket = ss.accept();

        InputStream is = socket.getInputStream();
        InputStreamReader isr=new InputStreamReader(is);

        int b;
        while ((b=isr.read())!=-1){
            System.out.print((char) b);
        }

        //返回client信息
        OutputStream os = socket.getOutputStream();
        os.write("到底因为什么这么高兴".getBytes());
        socket.shutdownOutput();


        socket.close();
        ss.close();
    }
}

输出结果

代码解析:PS:少一个shutdown结束标记

7.3 上传文件---网络IO流和本地IO流的混合。

只要用到bufferinputstream/outputstream 或者bufferreader/writer,就要在写出和读取之后刷新flush缓冲区buffer。

client:

java 复制代码
package com.InetAddress.exe.综合练习.test3;

import java.io.*;
import java.net.Socket;

public class client {
    public static void main(String[] args) throws IOException {
        //1.创建socket对象,并连接服务器
        Socket socket=new Socket("127.0.0.1",10086);

        //2.创建网络输出流+本地输入流读取本地文件
        OutputStream os = socket.getOutputStream();
        BufferedOutputStream bos=new BufferedOutputStream(os);

        BufferedInputStream bis=new BufferedInputStream(new FileInputStream("11-22new-project2\\00002576NLd.jpg"));

        //读取+写出
        int len;
        byte[] bytes=new byte[1024];
        while ((len=bis.read(bytes))!=-1){
            bos.write(bytes,0,len);
        }

        //往服务器写出结束标记
        socket.shutdownOutput();
        bis.close();

        //读取服务器返回的数据---是中文---用字符流
        InputStream is = socket.getInputStream();
        BufferedReader br=new BufferedReader(new InputStreamReader(is));
        String line=br.readLine();
        System.out.println(line);

        
        //释放资源
        socket.close();

    }
}

server:

java 复制代码
package com.InetAddress.exe.综合练习.test3;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    public static void main(String[] args) throws IOException {
        //创建对象并绑定端口
        ServerSocket ss=new ServerSocket(10086);

        //等待客户端来连接
        Socket socket = ss.accept();

        //读取网络输入流+写出本地输出流----图片---用字节流
        BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());

        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("11-22new-project2\\aaa\\bbb.jpg"));

        //开始读取并保存数据到本地文件
        int len;
        byte[] bytes=new byte[1024];
        while ((len=bis.read(bytes))!=-1){
            bos.write(bytes,0,len);
        }
        bos.close();

        //返回传输完毕的消息---是中文---用字符流
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("上传成功");//此时写到的是缓冲区上。要flush刷新一下,才会到client中
        bw.newLine();

        bw.flush();

        //释放资源
        socket.close();
        ss.close();

    }
}

输出结果

7.4解决每次保存文件名重复问题

采用UUID类的randomUUID方法来实现。

client代码不变,server代码的文件保存地址改变:

java 复制代码
package com.InetAddress.exe.综合练习.test3;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

public class server {
    public static void main(String[] args) throws IOException {
        //创建对象并绑定端口
        ServerSocket ss=new ServerSocket(10086);

        //等待客户端来连接
        Socket socket = ss.accept();

        //读取网络输入流+写出本地输出流----图片---用字节流
        BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());

        //改变文件保存地址
        String name = UUID.randomUUID().toString().replace("-", "");
        BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("11-22new-project2\\aaa\\"+name+".jpg"));

        //开始读取并保存数据到本地文件
        int len;
        byte[] bytes=new byte[1024];
        while ((len=bis.read(bytes))!=-1){
            bos.write(bytes,0,len);
        }
        bos.close();

        //返回传输完毕的消息---是中文---用字符流
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("上传成功");//此时写到的是缓冲区上。要flush刷新一下,才会到client中
        bw.newLine();

        bw.flush();

        //释放资源
        socket.close();
        ss.close();

    }
}

输出结果:

7.5 利用多线程实现上传文件

  1. 每运行一次client程序,就创建一个客户端,就会创建一条与服务器连接的通道。
  2. 所以,在服务器接收到客户端发来的socket时,就创建一个线程,来执行读取保存文件到本地的工作,这样一来,几个客户端线程就可以并发执行,不用保存完一个文件之后再保存另一个文件,可以一会儿在一个通道传递数据,一会儿去另一个通道传递数据。互相不会影响。
  3. 并且,不用同步代码块和锁,因为没有共享资源的争夺。
  4. 服务器server要在接收socket和开启线程作为循环,一直监听接收socket

client代码不变,server代码如下:

java 复制代码
package com.InetAddress.exe.综合练习.test3;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.UUID;

public class thread_server {
    public static void main(String[] args) throws IOException {
        //创建对象并绑定端口
        ServerSocket ss=new ServerSocket(10086);

        while (true) {
            //等待客户端来连接
            Socket socket = ss.accept();

            new Thread(new MyRun(socket)).start();
        }

    }
}

线程类:

java 复制代码
package com.InetAddress.exe.综合练习.test3;

import java.io.*;
import java.net.Socket;
import java.util.UUID;

public class MyRun implements Runnable{
    Socket socket;

    public MyRun(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            //读取网络输入流+写出本地输出流----图片---用字节流
            BufferedInputStream bis=new BufferedInputStream(socket.getInputStream());

            //改变文件保存地址
            String name = UUID.randomUUID().toString().replace("-", "");
            BufferedOutputStream bos=new BufferedOutputStream(new FileOutputStream("11-22new-project2\\aaa\\"+name+".jpg"));

            //开始读取并保存数据到本地文件
            int len;
            byte[] bytes=new byte[1024];
            while ((len=bis.read(bytes))!=-1){
                bos.write(bytes,0,len);
            }
            bos.close();

            //返回传输完毕的消息---是中文---用字符流
            BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
            try {
                bw.write("上传成功");//此时写到的是缓冲区上。要flush刷新一下,才会到client中
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            bw.newLine();

            bw.flush();

            //释放资源

        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

7.6 利用线程池来实现上传文件

创建完线程池后,向线程池提交任务,交由线程池里面的线程执行。参数是runnable类和callable类。

java 复制代码
package com.InetAddress.exe.综合练习.test4;

import com.InetAddress.exe.综合练习.test3.MyRun;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.server.SocketSecurityException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class server {
    public static void main(String[] args) throws IOException {

        //创建线程池:
        ThreadPoolExecutor pool=new ThreadPoolExecutor(
                3,
                16,
                60,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy()//线程已经超负荷要怎么拒绝再来的任务。
        );



        ServerSocket ss=new ServerSocket(10086);

        while (true){
            Socket socket = ss.accept();

            pool.submit(new MyRun(socket));

        }
    }
}

7.7 BS架构简单实现

在浏览器的网址输入栏中输入 127.0.0.1:10086,就会向server输入有关信息。

总结:

创建socket------获取socket的字节输入流/字节输出流-------(有中文则转为字符流)--------读取/写出数据------socket关闭

相关推荐
程序员zgh4 小时前
常用通信协议介绍(CAN、RS232、RS485、IIC、SPI、TCP/IP)
c语言·网络·c++
小汐睡着了4 小时前
解决虚拟机VMware与宿主机网络不通的问题-error
linux·网络·redhat
Heart_to_Yang6 小时前
Telnet 调试屏幕输出信息卡死问题解决
网络·windows·经验分享
资料库016 小时前
华为OSPF详解
网络·华为
yenggd6 小时前
锐捷路由器nat上网+ipsec配置案例
网络
liebe1*16 小时前
第九章 防火墙入侵防御
运维·网络·防火墙
sc.溯琛6 小时前
计算机网络试题分类及解析完整版
网络
wniuniu_7 小时前
RBD 客户端挂载操作指南
网络·ceph
橘子真甜~7 小时前
C/C++ Linux网络编程13 - 传输层TCP协议详解(面向字节流和有连接)
linux·运维·服务器·c语言·网络·c++·tcp/ip