在网络通信协议下,不同计算机上运行的程序进行数据的传输;封装在java.net包下

网络编程三要素:

  • IP:设备在网络中的地址,是唯一的标识
  • 端口号:应用程序在设备中的唯一标识
  • 协议:数据在网络中传输的规则 TCP、UDP、http、https、ftp

IP:Internet Protocol 互联网协议地址,常见的IP分为IPV4、IPV6

  1. IPV4 Internet Protocol version 4,互联网通信协议第四版

采用32为地址长度,分四组;采用点分十进制,每八个为一组转为十进制(0-255)

IPV4只有不到43亿个IP地址

  1. IPV6 Internet Protocol version 6

采取128位地址长度,分成8组,最多有2^128个IP,采用冒分十六进制,例如2001:0DB8:0000:0023:0008:0800:200C:417A

可以省略前面的0 : 2001:DB8:0:23:8:800:200C:417A

特殊情况:FF01:0:0:0:0:0:0:1101 采用0位压缩表示法 FF01::1101

地址分类

分为公网IP和私网IP

192.168开头的就是私有地址,范围即 192.168.0.0192.168.255.255,专门位组织机构内部使用

特殊IP : 127.0.0.1 ,也即localhost,是本地回环地址,永远只会寻找当前本机

假设192.168.1.100是我电脑的IP,这个IP与127.0.0.1是不一样的:

  • 192.168.1.100发送数据:数据经过路由器,路由器再发送到本机
  • 127.0.0.1(localhost)发送数据:数据发送到网卡时就直接发回自己,发不到路由器

三要素

InetAddress

This class represents an Internet Protocol (IP) address,该类表示IP的对象

classDiagram InetAddress <|– Inet4Address InetAddress <|– Inet6Address

该类没有对外提供构造方法,需要通过静态方法获取:

static InetAddress getByName(String host):确定主机名称的IP地址,主机名称可以是机器名称,也可以是IP地址

主机名称就是给自己电脑起的名字,

通过InetAddress对象就可以获取电脑名称或IP地址

InetAddress Iip = InetAddress.getByName("127.0.0.1");  
System.out.println("Iip.getHostAddress() = " + Iip.getHostAddress()); //127.0.0.1  
System.out.println("Iip.getHostName() = " + Iip.getHostName()); //localhost.sangfor.com.cn

getHostName() 可能因为网络原因 或者局域网没有这台电脑 是以IP形式体现的

端口号

应用程序在设备中的唯一标识

端口号:0-65535

其中0-1023之间的端口号用于一些知名的网络服务或者应用,自己可以使用的是1024以上的

协议

计算机网络中,连接和通信的规则被称为网络通信协议

应用层:HTTP、FTP、Telnet、DNS

传输层:TCP、UDP

网络层:IP、ICMP、ARP

UDP:

  • 用户数据报协议,面向无连接的协议
  • 速度快,有大小限制,一次最多发送64K,数据不安全,易丢失数据

TCP:

  • 传输控制协议,面向连接的协议
  • 速度慢,没有大小限制,数据安全

URL

java.net.URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它。 URL由4部分组成:协议、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。例如HTTP协议的默认端口为80。在浏览器中访问网页时,地址栏显示的地址就是URL。 URL标准格式为:<协议>://<域名或IP>:<端口>/<路径> 。其中,<协议>://<域名或IP>是必需的,<端口>/<路径>有时可省略。如:https://www.baidu.com

为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,该类封装了大量复杂的涉及从远程站点获取信息的细节,可以使用它的各种方法来对URL对象进行分割、合并等处理。

URL url = new URL("http://www.jd.com:8080/java/index.html?name=admin#tip");  
System.out.println("协议:" + url.getProtocol()); //协议:http  
System.out.println("域名:" + url.getHost()); //域名:www.jd.com  
System.out.println("获取该URL协议默认关联的端口:" + url.getDefaultPort()); //获取该URL协议默认关联的端口:80  
System.out.println("端口:" + url.getPort()); //端口:8080  
System.out.println("获取资源路径(不包含请求参数):" + url.getPath()); //获取资源路径(不包含请求参数):/java/index.html  
System.out.println("获取资源路径(包含请求参数):" + url.getFile()); //获取资源路径(包含请求参数):/java/index.html?name=admin  
System.out.println("获取参数:" + url.getQuery()); //获取参数:name=admin  
System.out.println("锚点:" + url.getRef()); //锚点:tip

使用java.net.URL类的InputStream openStream() 方法,还可以打开到此URL的连接并返回一个用于从该连接读入的InputStream,实现最简单的网络爬虫。

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        URL url = new URL("http://www.baidu.com/");
        InputStream ips = url.openStream();
        // 将字节流转换为字符流
        br = new BufferedReader(new InputStreamReader(ips));
        String str = null;
        // 这样就可以将网络内容下载到本地机器。
        // 然后进行数据分析,建立索引,这也是搜索引擎的第一步。  
        while ((str = br.readLine()) != null) {
            System.out.println(str);
        }
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (br != null) {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

TCP/UDP

Socket实际是传输层供给应用层的编程接口。Socket就是应用层与传输层之间的桥梁。使用Socket编程可以开发客户机和服务器应用程序,可以在本地网络上进行通信,也可通过Internet在全球范围内通信。

TCP协议和UDP协议是传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。

TCP

使用TCP协议,须先建立TCP连接,形成传输数据通道,似于拨打电话 传输前,采用“三次握手”方式,属于点对点通信,是面向连接的,效率低。 仅支持单播传输,每条TCP传输连接只能有两个端点(客户端、服务端)。 两个端点的数据传输,采用的是“字节流”来传输,属于可靠的数据传输。 传输完毕,需释放已建立的连接,开销大,速度慢,适用于文件传输、邮件等。

UDP

采用数据报(数据、源、目的)的方式来传输,无需建立连接,类似于发短信。 每个数据报的大小限制在64K内,超出64k可以分为多个数据报来发送。 发送不管对方是否准备好,接收方即使收到也不确认,因此属于不可靠的。 可以广播发送,也就是属于一对一、一对多和多对一连接的通信协议。 发送数据结束时无需释放资源,开销小,速度快,适用于视频会议、直播等。

描述 TCP UDP
是否连接 面向连接 面向非连接
传输可靠性 可靠 不可靠
连接对象个数 一对一 一对一、一对多、多对一
传输方式 面向字节流 面向报文
传输速度
应用场景 适用于实时应用(视频会议、直播等) 适用于可靠传输(文件传输、邮件等)

UDP通信

在UDP通信协议下,两台计算机之间进行数据交互,并不需要先建立连接,发送端直接往指定的IP和端口号上发送数据即可,但是它并不能保证数据一定能让对方收到,也不能确定什么时候可以送达。 java.net.DatagramSocket类和java.net.DatagramPacket类是使用UDP编程中需要使用的两个类,并且发送端和接收端都需要使用这个俩类,并且发送端与接收端是两个独立的运行程序。

  1. DatagramSocket:负责接收和发送数据,创建接收端时需要指定端口号。
  2. DatagramPacket:负责把数据打包,创建发送端时需指定接收端的IP地址和端口。

DatagramSocket

DatagramSocket类作为基于UDP协议的Socket,使用DatagramSocket类可以用于接收和发送数据,同时创建接收端时还需指定端口号。 DatagramSocket类的构造方法:

DatagramSocket类的构造方法:

方法名 描述
public DatagramSocket() 创建发送端的数据报套接字,随机端口号
public DatagramSocket(int port) 创建接收端的数据报套接字,并指定端口号

DatagramSocket类的常用方法:

方法名 描述
public void send(DatagramPacket p) 发送数据报。
public void receive(DatagramPacket p) 接收数据报。
public void close() 关闭数据报套接字。

DatagramPacket

DatagramPacket类负责把发送的数据打包(打包的数据为byte类型的数组),并且创建发送端时需指定接收端的IP地址和端口。 DatagramPacket类的构造方法:

方法名 描述
public DatagramPacket(byte buf[], int offset, int length) 创建接收端的数据报。
public DatagramPacket(byte buf[], int offset, int length, InetAddress address, int port) 创建发送端的数据报,并指定接收端的IP地址和端口号。

DatagramPacket类的常用方法:

方法名 描述
public synchronized byte[] getData() 返回数据报中存储的数据
public synchronized int getLength() 获得发送或接收数据报中的长度
  • 发送数据
  1. 创建发送端的DatagramSocket对象
  2. 数据打包 -> DatagramPacket
  3. 发送数据
  4. 释放资源
        //1. 创建DatagramSocket对象
        // 创建时绑定端口,通过这个端口向外发送数据
        // 空参 从可用端口中随机获取一个
        DatagramSocket socket = new DatagramSocket();

        //2. 打包数据
        byte[] bytes = "你好!".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, 	
                                                   bytes.length,
                                                   InetAddress.getLocalHost(),
                                                   10086);
        
        //3. 发送数据
        socket.send(packet);
        
        //4. 关闭连接
        socket.close();
  • 接收数据
  1. 创建接收端的DatagramSocket对象,监听发送端指定的本机(接收端)端口
  2. 接收打包好的数据
  3. 解析Packet
  4. 释放资源
DatagramSocket datagramSocket = new DatagramSocket(12345);  
byte[] buf = new byte[64 * 1024];  
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);  
  
//接收数据,阻塞接收  
datagramSocket.receive(datagramPacket);  
  
byte[] data = datagramPacket.getData();  //data == buf is true
System.out.println(new String(data));

但是这样做会有一个问题,应该接收多少数据就转化为多少长度的字符串,使用getLength就可以解决:

DatagramSocket datagramSocket = new DatagramSocket(12345);  
byte[] buf = new byte[64 * 1024];  
DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);  
  
//接收数据,阻塞接收  
datagramSocket.receive(datagramPacket);  
  
byte[] data = datagramPacket.getData();  
System.out.println(new String(data,0,datagramPacket.getLength()));  
System.out.println(data == buf);  
System.out.println("发送方的IP是:" + datagramPacket.getAddress() + " , 端口是:" + datagramPacket.getPort()); //发送方的IP是:/2.0.0.1 , 端口是:63950
  • 在运行时应该先运行接收端,再运行发送端,接收端的receive方法会阻塞当前线程

多发多收

  • 发送端
DatagramSocket socket = new DatagramSocket();  
Scanner sc = new Scanner(System.in);  
String line;  
byte[] data = new byte[10];  
DatagramPacket packet = new DatagramPacket(data, 10, InetAddress.getLocalHost(), 12345);  
while (!(line = sc.nextLine()).equals("end conn")){  
    packet.setData(line.getBytes());  
    packet.setLength(line.getBytes().length);  
    socket.send(packet);  
}
  • 接收端
DatagramSocket socket = new DatagramSocket(12345);  
byte[] data = new byte[1024 * 64];  
DatagramPacket packet = new DatagramPacket(data, data.length);  
String line;  
  
do {  
    socket.receive(packet);  
    line = new String(packet.getData(),0, packet.getLength());  
    System.out.print("from " + packet.getAddress() + ":" + packet.getPort() + " ");  
    System.out.println(line);  
}while (!line.equals("end conn"));

通信方式

  1. 单播:以前的代码就是单播
  2. 组播:组播地址:224.0.0.0239.255.255.255 其中224.0.0.0224.0.0.255为预留的组播地址
  3. 广播:255.255.255.255 局域网中所有电脑

组播:MulticastSocket

组播发送数据:

        MulticastSocket mcSocket = new MulticastSocket();
        
        byte[] bytes = "你好".getBytes();
        InetAddress mcIP = InetAddress.getByName("224.0.0.1");//指定组播地址
        DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length, mcIP, 10086);
        
        mcSocket.send(packet);
        
        mcSocket.close();

组播接收数据:

        MulticastSocket mcSocket = new MulticastSocket(10086); //指定接收哪个端口

        InetAddress mcIP = InetAddress.getByName("224.0.0.1");
        byte[] bytes = new byte[1024];
        DatagramPacket packet = new DatagramPacket(bytes, 0, bytes.length);

		/*将本机加入组播地址*/
        mcSocket.joinGroup(mcIP);
        mcSocket.receive(packet);

        System.out.println(packet.getData());

广播发送数据:

TCP通信

TCP是一种可靠的网络协议,它在通信的两端各建立一个Socket对象,通信之前要保证连接已经建立,通过Socket产生IO流来进行通信

套接字是一种进程间的数据交换机制,利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。 在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client),而在第一次通讯中等待连接的程序被称作服务端(Server)。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。 套接字与主机地址和端口号相关联,主机地址就是客户端或服务器程序所在的主机的IP地址,端口地址是指客户端或服务器程序使用的主机的通信端口。在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样客户端和服务器通过套接字所建立连接并使用IO流进行通信。

三次握手和四次挥手

  • 三次握手:确保连接建立

  • 四次挥手:确保连接断开,且数据处理完毕

半关闭

`void close()`

 Socket socket = new Socket("127.0.0.1",10000);

连接本机的10000端口,如果连接不上代码会报错,但是此时直接运行会报错:

因为服务器的代码还没写,会直接报错。

  • 客户端:
        //1. 创建Socket对象 连接服务器的端口
        //   如果此时连接不上 代码报错
        Socket socket = new Socket("127.0.0.1",10000);

        //2. 从连接通道中获取输出流
        OutputStream ops = socket.getOutputStream();

        //写出数据
        byte[] bytes = "hello world".getBytes();
        ops.write(bytes,0, bytes.length);

        //3.释放资源
        ops.close(); //关闭流
		socket.close(); //断开连接

注意:

  • 服务器:
        //1. 创建ServerSocket对象
        ServerSocket serverSocket = new ServerSocket(10000);

        //2.获取客户端的Socket 也就是客户端与服务器的连接
        // 阻塞方法
        Socket socket = serverSocket.accept();
        //从连接通道中获取数据
        InputStream ips = socket.getInputStream();

        byte[] bytes = new byte[1024];
        int readCount = ips.read(bytes);
        System.out.println(new String(bytes,0,readCount));

        //4. 释放资源
        socket.close(); //断开客户端连接
        serverSocket.close(); //关闭服务器

注意:

注意:read()方法会阻塞线程

半关闭

中文乱码问题

如果在服务器使用字节流读取,中文就会产生乱码问题。

需要在服务器端使用字符流读取数据:

如果想进一步提高效率,可以再包装一层缓冲流:

此时输出端必须每次输出结束都写入一个换行符

三次握手和四次挥手

  • 三次握手:确保连接建立

  • 四次挥手:确保连接断开,且数据处理完毕

基于TCP协议的编程

TCP协议编程的概述

套接字是一种进程间的数据交换机制,利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准。
在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client),而在第一次通讯中等待连接的程序被称作服务端(Server)。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
套接字与主机地址和端口号相关联,主机地址就是客户端或服务器程序所在的主机的IP地址,端口地址是指客户端或服务器程序使用的主机的通信端口。在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样客户端和服务器通过套接字所建立连接并使用IO流进行通信。

Socket类的概述

Socket类实现客户端套接字(Client),套接字是两台机器间通信的端点。
Socket类的构造方法:

方法名 描述
public Socket(InetAddress a, int p) 创建套接字并连接到指定IP地址的指定端口号

Socket类的成员方法:

方法名 描述
public InetAddress getInetAddress() 返回此套接字连接到的远程 IP 地址。
public InputStream getInputStream() 返回此套接字的输入流(接收网络消息)。
public OutputStream getOutputStream() 返回此套接字的输出流(发送网络消息)。
public void shutdownInput() 禁用此套接字的输入流
public void shutdownOutput() 禁用此套接字的输出流。
public synchronized void close() 关闭此套接字(默认会关闭IO流)。

ServerSocket类的概述

ServerSocket类用于实现服务器套接字(Server服务端)。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
ServerSocket类的构造方法:

方法名 描述
public ServerSocket(int port) 创建服务器套接字并绑定端口号

ServerSocket类的常用方法:

方法名 描述
public Socket accept() 侦听要连接到此套接字并接受它。
public InetAddress getInetAddress() 返回此服务器套接字的本地地址。
public void close() 关闭此套接字。

TCP单向通讯的实现

Java语言的基于套接字编程分为服务端编程和客户端编程,其通信模型如图所示:

服务器端实现步骤

  1. 创建ServerSocket对象,绑定并监听端口;
  2. 通过accept监听客户端的请求;
  3. 建立连接后,通过输出输入流进行读写操作;
  4. 调用close()方法关闭资源。

【示例】TCP:单向通信之服务端

public class Test01 {
    public static void main(String[] args)  {
        ServerSocket serverSocket = null;
        Socket accept = null;
        try {
            // 实例化ServerSocket对象(服务端),并明确服务器的端口号
            serverSocket = new ServerSocket(8888);
            System.out.println("服务端已启动,等待客户端连接..");
            // 使用ServerSocket监听客户端的请求
            accept = serverSocket.accept();
            // 通过输入流来接收客户端发送的数据
            InputStreamReader reader = new InputStreamReader(accept.getInputStream());
            char[] chars = new char[1024];
            int len = -1;
            while ((len = reader.read(chars)) != -1) {
                System.out.println("接收到客户端信息:" + new String(chars, 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (accept != null) {
                try {
                    accept.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意:socket对象调用了close()方法之后,那么该socket对象就不能再使用了!

客户端实现步骤

  1. 创建Socket对象,指定服务端的地址和端口号;
  2. 建立连接后,通过输入输出流进行读写操作;
  3. 通过输出输入流获取服务器返回信息;
  4. 调用close()方法关闭资源。

【示例】TCP:单向通信之客户端

public class Test02 {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            // 实例化Socket对象(客户端),并明确连接服务器的IP和端口号
            InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
            socket = new Socket(inetAddress, 8888);
            // 获得该Socket的输出流,用于发送数据
            Writer writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write("为中华之崛起而读书!");
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭资源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

注意:一定是先启动服务器程序,然后再启动客户端程序,先后循序千万别弄混了!

TCP双向通讯的实现

上一个章节我们掌握了Socket的单项通讯,那么如何实现Socket的双向通讯呢?在本章节我们将讲解的讲解双向通讯的实现。
在双向通讯的案例中,客户端需要向服务端发送一张图片,服务端收到客户端发送的图片后,则需要向客户端回复收到图片的反馈。在客户端给服务端发送图片的时候,图片发送完毕必须调用shutdownOutput()方法来关闭socket输出流,否则服务端读取数据就会一直阻塞。

服务器端实现步骤

  1. 创建ServerSocket对象,绑定监听端口;
  2. 通过accept()方法监听客户端请求;
  3. 使用输入流接收客户端发送的图片,然后通过输出流保存图片
  4. 通过输出流返回客户端图片收到。
  5. 调用close()方法关闭资源

【示例】TCP:双向通信之服务端

public class Test01 {
    public static void main(String[] args) {
        Socket socket = null;
        ServerSocket serverSocket = null;
        try {
            // 1.创建ServerSocket对象(客户端),并明确端口号
            serverSocket = new ServerSocket(8889);
            System.out.println("服务端已启动,等待客户端连接..");
            // 2.使用ServerSocket监听客户端的请求
            socket = serverSocket.accept();
            // 3.使用输入流接收客户端发送的图片,然后通过输出流保存图片
            InputStream inputStream = socket.getInputStream();
            byte[] bytes = new byte[1024];
            int len = -1;
            FileOutputStream fos = new FileOutputStream("./socket/images/yaya.jpeg");
            while ((len = inputStream.read(bytes)) != -1) {
                fos.write(bytes, 0, len);
            }
            // 4.给客户端反馈信息
            Writer osw = new OutputStreamWriter(socket.getOutputStream());
            BufferedWriter bw = new BufferedWriter(osw);
            bw.write("图片已经收到,谢谢");
            bw.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5.关闭资源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (serverSocket != null) {
                try {
                    serverSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

客户端实现步骤

  1. 创建socket对象,指明需要连接的服务器地址和端口号;
  2. 建立连接后,通过输出流向服务器端发送图片;
  3. 通过输入流获取服务器的响应信息;
  4. 调用close()方法关闭资源

【示例】TCP:双向通信之客户端

public class Test02 {
    public static void main(String[] args) {
        Socket socket = null;
        try {
            // 1.实例化Socket对象(客户端),并设置连接服务器的IP和端口号
            socket = new Socket(InetAddress.getByName("127.0.0.1"), 8889);
            // 2.通过输入流读取图片,然后再通过输出流来发送图片
            OutputStream outputStream = socket.getOutputStream();
            FileInputStream fis = new FileInputStream("./socket/images/tly.jpeg");
            byte[] bytes = new byte[1024];
            int len = -1;
            while ((len = fis.read(bytes)) != -1) {
                outputStream.write(bytes, 0, len);
            }
            // 注意:此处必须关闭Socket的输出流,来告诉服务器图片发送完毕
            socket.shutdownOutput();
            // 3.接收服务器的反馈
            InputStreamReader isr = new InputStreamReader(socket.getInputStream());
            BufferedReader reader = new BufferedReader(isr);
            String lineStr = null;
            while ((lineStr = reader.readLine()) != null) {
                System.out.println("服务器端反馈:" + lineStr);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4.关闭资源
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

练习

多发多收

  • 客户端:
class Client{
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10001);
        String line;
        Scanner sc = new Scanner(System.in);
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

        while (true){
            line = sc.nextLine();
            bw.write(line);
            bw.flush(); /*必须调用flush刷新*/
            if ("#end conn".equals(line)) break;
        }
        socket.close();
    }
}

如果在此处没有用flush()刷新,用户的输入都存入了缓冲区当中,最终输入#end conn时,bw关闭会将所有数据一次性输出,服务器一次将数据全部接收(本例设置为1024个字符),而客户端已经停止运行了,服务器端无法停止:

  • 服务器
class Server{
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10001);
        Socket socket = serverSocket.accept();
        String hostIP = socket.getInetAddress().getHostAddress();
        int port = socket.getPort();
        InputStreamReader isr = new InputStreamReader(socket.getInputStream());

        char[] chars = new char[1024];
        while (true){
            int readCount = isr.read(chars);/*阻塞*/
            if (readCount != -1){
                String msg = new String(chars, 0, readCount);
                System.out.println(hostIP + "/" + port + " : " + msg);
                if ("#end conn".equals(msg)) break;
            }
        }

        isr.close();
        socket.close();
        serverSocket.close();
    }
}

采用InputStreamReader接收数据是因为:在输出时虽然采用BufferedWriter进行输出,但是输出完毕后并没有newLine(),也就是说在服务器如果采用BufferedReader接收数据的话,只有在客户端断开连接时(#end conn)才会读到一行数据(实际上是客户端发出所有数据的拼接)

而在这里采用InputStreamReader,转换流底层有缓冲区,read每次从缓冲区中读取数据,如果缓冲区读取完毕就从管道中再次获取尽可能多的数据存入字节缓冲区;每次从缓冲区中读出1024个字符,缓冲区每次从管道中获取8192个字节。

可以在客户端每次输出后都调用newLine()方法:

这样在服务器就可以使用BufferedReader接收数据了

注意:

在判断是否结束时使用 -1 来判断,但是如果客户端还未发送数据的话,此时是读不到任何数据的,也就是read方法也会阻塞当前线程。

在客户端断开连接后,SocketInputStream的成员eof会被置为true,read()方法里会去判断eof为true是就返回-1

在发送端中,如果输入结束指令#end conn ,跳出循环后直接关闭socket,eof被置为true;在接收端就需要判断read返回值 = -1结束

常见异常

  • java.net.SocketException: Connection reset一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常

客户端:

服务器:

客户端退出后未关闭连接,服务器仍连续读取数据,就会报该异常

  1. 如果客户端退出后关闭了连接,服务器此时在连续读取数据,由于每次调用read方法都会判断eof变量,所以每次都会返回-1,在服务器端应该使用readCount来判断是否结束,也就是说服务器端不需要对结束指令#end conn进行判断,只需要判断read方法的返回值

  2. 对于服务器使用BufferedReader读取数据的情况,如果客户端退出并关闭连接,服务器会一直读取 null,这就是服务器端的结束条件,如果客户端退出时没有关闭连接,还是会报该异常

  • java.net.SocketException: Connect reset by peer:如果一端的Socket被关闭(或主动关闭,或因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常

接收和反馈

如果对客户端返回消息,需要指定客户端的接收端口号,本例中:

也可以:

  • 服务器:

  • 客户端

最终客户端和服务器都需要对自身的两个端口进行关闭

注意:在服务器向客户端发送通知时连接只能开辟一次

其他的实现方式:(客户端只发送一次,发送完毕等待服务器通知;服务器端接收到消息后对客户端进行通知)

只有客户端在断开连接时read才能读到-1,只有返回-1才能结束服务器阻塞状态,但是如果在此时断开客户端连接

可以在此处调用socket.shutdownOutput()

半关闭问题

问题背景:客户端连接服务器,发送一个请求,捕获响应信息。

客户端通过输出流向服务器发送数据, 如果不关闭输出流,服务器无法判断出客户端是否已经输出完毕,因此服务器的读操作将会处于阻塞状态;当客户端关闭输出流,服务器得到客户端的输出已经结束的信息,服务器开始执行读操作。

然而,这会导致另外一个问题,客户端输出流关闭的时候,socket 也会自动断开连接。当服务器需要通过输出流向客户端传输数据时,便会出现java.net.SocketException: Socket closed

解决方案:使用 半关闭:

// dos2.close(); // 会导致 socket 连接断开
 
socket.shutdownOutput();
// 现在 socket 是半关闭状态,输出流关闭,但输入流打开,socket 连接不会断开。

以下内容摘自《Java 核心技术卷2》第三章

2.2 半关闭

套接字连接的一端可以终止其输出,同时仍旧可以接受来自另一端的数据。

这是一种很典型的情况,例如我们在向服务器传输数据,但并不知道要传输多少个数据。如果关闭一个套接字,那么服务器的连接将立刻断开,因而也就无法读取服务器的响应了。

使用半关闭的方法就可以解决上述的问题,可以通过关闭一个套接字的输出流来表示发送给服务器的请求数据已经结束,但是必须保持输入流处于打开状态。

socket.shutdownOutput();
socket.shutdownInput();

当然,该协议只适用于一站式的服务,例如 HTTP 服务,在这种服务中,客户端连接服务器,发送一个请求,捕获响应信息,然后断开连接。

上传文件

  • 服务器
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10001);
        Socket socket = serverSocket.accept();
        InputStream ips = socket.getInputStream();
        FileOutputStream fos = new FileOutputStream("copy_names.txt");
        int readCount;
        byte[] bytes = new byte[1024 * 2];
        while ((readCount = ips.read(bytes)) != -1){
            fos.write(bytes,0,readCount);
        }
        
        OutputStream ops = socket.getOutputStream();
        ops.write("done".getBytes());
        ops.flush();

        fos.close();
        ips.close();
        socket.close();
        serverSocket.close();
    }
  • 客户端
	 public static void main(String[] args) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10001);
        FileInputStream fis = new FileInputStream("names.txt");
        OutputStream ops = socket.getOutputStream();
        byte[] bytes = new byte[1024 * 2];
        int readCount;
        while ((readCount = fis.read(bytes)) != -1){
            ops.write(bytes,0,readCount);
        }
        socket.shutdownOutput();

        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(reader.readLine());

        ops.close();
        fis.close();
        socket.close();
    }

注意:

否则服务器的循环无法终止:

如果客户端向服务器单向发送一次数据,发送完成之后记得发送结束标记

拷贝文件时使用的流最后一定要关闭

上传的文件名重复

java.util.UUID,表示通用唯一标识符的类,UUID标识一个128位的值

通过UUID.randomUUID()可以获取一个随机的UUID字符串

System.out.println(UUID.randomUUID());//97e1f3f4-1257-4309-befa-e5dff0879692
System.out.println(UUID.randomUUID().toString().replaceAll("-",""));//97e1f3f412574309befae5dff0879692

多线程上传

思路:在服务器端将请求连接的每一个用户看作一个线程,分别对这些线程进行操作

  • 客户端:
class Client {
    public static void doSome(File file) throws IOException {
        Socket socket = new Socket("127.0.0.1", 10008);
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
        byte[] bytes = new byte[1024 * 2];
        int readCount;
        while ((readCount = bis.read(bytes)) != -1){
            bos.write(bytes,0,readCount);
        }
        bos.flush();
        socket.shutdownOutput();
        BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        System.out.println(reader.readLine());
        socket.close();
    }
}
  • 服务器:
class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(10008);
        while (true){
            Socket socket = serverSocket.accept();
            new Thread(new MyRunnable(socket)).start();
        }
    }
}

注意:

  • 线程类:
public class MyRunnable implements Runnable{
    Socket socket;
    public MyRunnable(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        try {
            String name = UUID.randomUUID().toString().replaceAll("-", "");

            String hostAddress = socket.getInetAddress().getHostAddress();
            String hostName = socket.getInetAddress().getHostName();

            BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
            BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("serverdir/" + name + ".txt"));
            byte[] bytes = new byte[1024 * 2];
            int readCount;
            while ((readCount = bis.read(bytes)) != -1){
                bos.write(bytes,0,readCount);
            }
            bos.close();
            socket.getOutputStream().write((Thread.currentThread().getName() + "@" + hostAddress + "@" + hostName + "接收完毕").getBytes());
            socket.close();
        } catch (IOException e){
            e.printStackTrace();
        }
    }
}

注意:Server不停止运行,每个线程的线程号都是依次向上累加

线程池上传

只需要将Server端的提交方式改为:

BS 接收浏览器的数据并打印

客户端就是浏览器,我们只需要在服务器接收数据就可以了

浏览器中访问指定的端口号,服务器就能打印出数据:

GET / HTTP/1.1
Host: localhost:10000
Connection: keep-alive
sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36 Edg/112.0.1722.39
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-US;q=0.7,en-GB;q=0.6,en-GB-oxendict;q=0.5
Cookie: Webstorm-3a48b5d7=0f49d683-9f8a-41cb-b29b-af0cf5bba784
x-forwarded-for: 8.8.8.8

聊天室

多收多发

public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目标:客户端开发: 多发多收  
        // 1、创建一个Socket通信管道与服务端建立可靠链接  
        Socket socket = new Socket("127.0.0.1", 9999);  
        // 2、从socket通信管道中得到一个字节输出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把字节输出流包装成一个数据输出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
        while (true) {  
            System.out.println("请说:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
  
            // 4、写数据出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
  
    }  
}
public class TcpServerDemo2 {  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服务端程序============");  
            // 目标:服务端的开发。  
            // 1、注册端口  
            ServerSocket serverSocket = new ServerSocket(9999);  
            // 2、监听客户端的链接请求,得到服务端socket  
            Socket socket = serverSocket.accept();  
            System.out.println("有人上线了~~~");  
            // 3、从服务端中获取一个字节输入流。  
            InputStream is = socket.getInputStream();  
            // 4、把字节输入流包装成数据输入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、读数据  
                String msg = dis.readUTF();  
  
                System.out.println("服务端收到了:");  
                System.out.println(msg);  
  
                System.out.println("对方IP:"  
                  + socket.getInetAddress().getHostAddress());  
                System.out.println("对方端口:"  
                        + socket.getPort());  
            }  
        } catch (Exception e) {  
            System.out.println("有人下线了");  
        }  
    }  
}
  • 多收多发
public class ServerReaderThread extends Thread{  
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、从服务端中获取一个字节输入流。  
            InputStream is = socket.getInputStream();  
            // 4、把字节输入流包装成数据输入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、读数据  
                String msg = dis.readUTF();  
                System.out.println(socket.getInetAddress().getHostAddress() + " 说:" + msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下线了!");  
        }  
    }  
}
public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目标:客户端开发: 多发多收  
        // 1、创建一个Socket通信管道与服务端建立可靠链接  
        Socket socket = new Socket("127.0.0.1", 9999);  
        // 2、从socket通信管道中得到一个字节输出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把字节输出流包装成一个数据输出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
        while (true) {  
            System.out.println("请说:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
            // 4、写数据出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
    }  
}
public class TcpServerDemo2 {  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服务端程序============");  
            // 目标:服务端的开发。  
            // 1、注册端口  
            ServerSocket serverSocket = new ServerSocket(9999);  
            while (true) {  
                // 2、监听客户端的链接请求,得到服务端socket  
                Socket socket = serverSocket.accept();  
                System.out.println(socket.getInetAddress().getHostAddress() + "上线了~!");  
                // 3、把这个客户端管道交给一个独立的子线程来处理。  
                new ServerReaderThread(socket).start();  
            }  
        } catch (Exception e) {  
           e.printStackTrace();  
        }  
    }  
}
  • 群聊
public class TcpClientDemo1 {  
    public static void main(String[] args) throws Exception {  
  
        // 目标:客户端开发: 多发多收  
        // 1、创建一个Socket通信管道与服务端建立可靠链接  
        Socket socket = new Socket("127.0.0.1", 9999);  
  
        // 立即为这个客户端管道分配一个独立的线程专门负责这个管道的收消息。  
        new ClientReaderThread(socket).start();  
  
        // 2、从socket通信管道中得到一个字节输出流。  
        OutputStream os = socket.getOutputStream();  
        // 3、把字节输出流包装成一个数据输出流  
        DataOutputStream dos = new DataOutputStream(os);  
  
        Scanner sc = new Scanner(System.in);  
  
        while (true) {  
            System.out.println("请说:");  
            String msg = sc.nextLine();  
  
            if("exit".equals(msg)) {  
                System.out.println("退出成功!~");  
                socket.close();  
                break;  
            }  
            // 4、写数据出去  
            dos.writeUTF(msg);  
            dos.flush();  
        }  
  
    }  
}
public class TcpServerDemo2 {  
  
    // 定义一个在线集合存储全部的在线socket管道。  
    public static List<Socket> onLineSockets = new ArrayList<>();  
  
    public static void main(String[] args) throws Exception {  
        try {  
            System.out.println("=========服务端程序============");  
            // 目标:服务端的开发。  
            // 1、注册端口  
            ServerSocket serverSocket = new ServerSocket(9999);  
            while (true) {  
                // 2、监听客户端的链接请求,得到服务端socket  
                Socket socket = serverSocket.accept();  
                System.out.println(socket.getInetAddress().getHostAddress() + "上线了~!");  
                onLineSockets.add(socket);  
                // 3、把这个客户端管道交给一个独立的子线程来处理。  
                new ServerReaderThread(socket).start();  
            }  
        } catch (Exception e) {  
           e.printStackTrace();  
        }  
    }  
}
public class ClientReaderThread extends Thread{  
    private Socket socket;  
    public ClientReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、从服务端中获取一个字节输入流。  
            InputStream is = socket.getInputStream();  
            // 4、把字节输入流包装成数据输入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、读数据  
                String msg = dis.readUTF();  
                // 把这个消息转发给当前在线的全部socket管道接收。  
                System.out.println("收到:" + msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println("客户端完成正常退出!");  
        }  
    }  
  
  
}
public class ServerReaderThread extends Thread{  
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 3、从服务端中获取一个字节输入流。  
            InputStream is = socket.getInputStream();  
            // 4、把字节输入流包装成数据输入流  
            DataInputStream dis = new DataInputStream(is);  
  
            while (true) {  
                // 5、读数据  
                String msg = dis.readUTF();  
                System.out.println(socket.getInetAddress().getHostAddress() + " 说:" + msg);  
                // 把这个消息转发给当前在线的全部socket管道接收。  
                sendMsgToAll(msg);  
                System.out.println("--------------------------------------");  
            }  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下线了!");  
        }  
    }  
  
    private void sendMsgToAll(String msg) throws Exception {  
        // 遍历在线集合的每个socket,把消息推给人家  
        for (Socket onLineSocket : TcpServerDemo2.onLineSockets) {  
            // 这个管道不能是自己,就应该发消息给他。  
            if(onLineSocket != socket) {  
                DataOutputStream dos = new DataOutputStream( onLineSocket.getOutputStream() );  
                dos.writeUTF(msg);  
                dos.flush(); // 刷出去消息。  
            }  
        }  
    }  
}

简易BS架构

BS架构是基于浏览器/服务器的,请求协议基于TCP,HTTP协议也是长连接,只是通信的时长较短

浏览器的每个请求,服务器都开启一个新的线程进行处理。

BS架构的基本原理:

  • 客户端使用浏览器发起请求

  • 服务器必须返回HTTP协议规定好的数据格式,否则浏览器无法识别,参照[[HTTP|HTTP的格式]]

//1. 服务器开启服务
ServerSocket serverSocket = new ServerSocket(8080);  
while (true) {  
    // 2、监听浏览器请求的管道链接。  
    Socket socket = serverSocket.accept();  
    // 3、交给一个独立的线程负责为这个管道响应一个网页回去。  
    new ServerReaderThread(socket).start();  
}
public class ServerReaderThread extends Thread{  //继承Thread子类,重写run方法
    private Socket socket;  
    public ServerReaderThread(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
        try {  
            // 响应一个网页给 socket 管道。  
            PrintStream ps = new PrintStream(socket.getOutputStream());  
            ps.println("HTTP/1.1 200 OK");  
            ps.println("Content-Type:text/html;charset=UTF-8");  
            ps.println(); // 必须换行  
            ps.println("<div style='color:red;font-size:80px'>我爱磊哥</div>");  
  
            ps.close();  
            socket.close();  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下线了!");  
        }  
    }  
}

注意:继承Thread的子类,未启动是任务,启动是线程

但是程序也有改进的地方,HTTP连接通信时间极短,非常使用线程池优化

ExecutorService pool = new ThreadPoolExecutor(3, 5, 5, 
											  TimeUnit.SECONDS  , 
											  new ArrayBlockingQueue<>(5),
											  Executors.defaultThreadFactory(),  
											  new ThreadPoolExecutor.AbortPolicy());  
// 1、注册端口  
ServerSocket serverSocket = new ServerSocket(8080);  
while (true) {  
    // 2、监听浏览器请求的管道链接。  
    Socket socket = serverSocket.accept();  
    // 把这个管道包装成一个任务对象,交给线程池排队  
    pool.execute(new ServerRunnable(socket));  //Thread就是Runnable
}
public class ServerRunnable implements Runnable{  
    private Socket socket;  
    public ServerRunnable(Socket socket) {  
        this.socket = socket;  
    }  
  
    @Override  
    public void run() {  
  
        try {  
            // 响应一个网页给 socket 管道。  
            PrintStream ps = new PrintStream(socket.getOutputStream());  
            ps.println("HTTP/1.1 200 OK");  
            ps.println("Content-Type:text/html;charset=UTF-8");  
            ps.println(); // 必须换行  
            ps.println("<div style='color:red;font-size:80px'>我爱磊哥</div>");  
  
            ps.close();  
            socket.close();  
        } catch (Exception e) {  
            System.out.println(socket.getInetAddress().getHostAddress()  
               + "下线了!");  
        }  
    }  
}
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。