【黑马java基础】网络通信

可以让设备中的程序与网络上其他设备中的程序进行数据交互(实现网络通信的)。

java提供了java.net.*包下提供了网络编程的解决方案

通信的基本架构主要有两种形式:一种是CS架构(Client 客户端/Server服务端)、一种是BS架构(Brower 浏览器/Server服务端)。

  • CS架构的特点:CS架构需要用户在自己的电脑或者手机上安装客户端软件,然后由客户端软件通过网络连接服务器程序,由服务器把数据发给客户端,客户端就可以在页面上看到各种数据了。
  • **BS架构的特点:**BS架构不需要开发客户端软件,用户只需要通过浏览器输入网址就可以直接从服务器获取数据,并由服务器将数据返回给浏览器,用户在页面上就可以看到各种数据了。

    这两种结构不管是CS、还是BS都是需要用到网络编程的相关技术。我们学习Java的程序员,以后从事的工作方向主要还是BS架构的。

一、网络通信三要素

三要素分别是IP地址、端口号、通信协议

  1. IP地址:表示设备在网络中的地址,是网络中设备的唯一标识

  2. 端口号:应用程序在设备中唯一的标识

  3. 协议:连接和数据在网络中传输的规则。

如下图所示:假设现在要从一台电脑中的微信上,发一句"你愁啥?"到其他电脑的微信上,流程如下

java 复制代码
1.先通过ip地址找到对方的电脑
2.再通过端口号找到对方的电脑上的应用程序
3.按照双方约定好的规则发送、接收数据

1.1 IP地址

**IP(Ineternet Protocol)全称互联网协议地址,是分配给网络设备的唯一表示。**IP地址分为:IPV4地址、IPV6地址

IP地址分类

IPV4地址由32个比特位(4个字节)组成,如果下图所示,但是由于采用二进制太不容易阅读了,于是就将每8位看成一组,把每一组用十进制表示(叫做点分十进制表示法)。所以就有了我们经常看到的IP地址形式,如:192.168.1.66

如果想查看本机的IP地址,可以在CMD命令行窗口,输入ipconfig命令查看,如下图所示:

经过不断的发展,现在越来越多的设备需要联网,IPV4地址已经不够用了,所以扩展出来了IPV6地址。

IPV6采用128位二进制数据来表示(16个字节),号称可以为地球上的每一粒沙子编一个IP地址,

IPV6比较长,为了方便阅读,每16位编成一组,每组采用十六进制数据表示,然后用冒号隔开(称为冒分十六进制表示法),如下图所示

现在的网络设备,一般IPV4和IPV6地址都是支持的。


域名

聊完什么是IP地址和IP地址分类之后,接下来再给大家介绍一下和IP地址相关的一个东西,叫做域名。

我们在浏览器上访问某一个网站是,就需要在浏览器的地址栏输入网址,这个网址的专业说法叫做域名。比如:传智播客的域名是http://www.itcast.cn

域名和IP其实是一一对应的,由运营商来管理域名和IP的对应关系。我们在浏览器上敲一个域名时,首先会在我们电脑的DNS服务器上找有没有记录过的域名,有的话,返回给真实IP地址进行访问;如果域名没有记录过,由运营商的域名解析服务,把域名转换为ip地址,再通过IP地址去访问对应的服务器设备。

公网IP,内网IP

  • 公网IP:是可以连接互联网的IP地址;内网IP:也叫局域网IP,只能组织机构内部使用。

  • 192.168. 开头的就是常见的局域网地址,范围即为192.168.0.0--192.168.255.255,专门为组织机构内部使用。

  • 特殊的IP地址:127.0.0.1、localhost:代表本机IP,只会寻找当前所在的主机。

IP常用命令:

ipconfig:查看本机IP地址。

ping IP地址:检查网络是否连通。

InetAddress

代表IP地址,常用方法有:

名称 说明
public static InetAddress getLocalHost() 获取本机IP,会以一个inetAddress的对象返回
public static InetAddress getByName(String host) 根据ip地址或者域名,返回一个inetAdress对象
public String getHostName() 获取该ip地址对象对应的主机名。
public String getHostAddress() 获取该ip地址对象中的ip地址信息。
public boolean isReachable(int timeout) 在指定毫秒内,判断主机与该ip对应的主机是否能连通

代码如下:

java 复制代码
package com.itheima.d1_ip;

import java.net.InetAddress;

/**
 * 目标:掌握InetAddress类的使用。
 */
public class InetAddressTest {
    public static void main(String[] args) throws Exception {
        // 1、获取本机IP地址对象的
        InetAddress ip1 = InetAddress.getLocalHost();
        System.out.println(ip1.getHostName());
        System.out.println(ip1.getHostAddress());

        // 2、获取指定IP或者域名的IP地址对象。
        InetAddress ip2 = InetAddress.getByName("www.baidu.com");
        System.out.println(ip2.getHostName());
        System.out.println(ip2.getHostAddress());

        // ping www.baidu.com
        System.out.println(ip2.isReachable(6000));
    }
}

//
ZHAO
192.168.2.49
www.baidu.com
183.2.172.42
true

百度的ip地址(183.2.172.42)访问不到,可能是因为CDN技术

1.2 端口号

端口号:指的是计算机设备上运行的应用程序的标识,被规定为一个16位的二进制数据,范围(0~65535)

端口号分为一下几类(了解一下)

  • 周知端口:0~1023,被预先定义的知名应用程序占用(如:HTTP占用80,FTP占用21)
  • 注册端口:1024~49151,分配给用户经常或者某些应用程序
  • 动态端口:49152~65536,之所以称为动态端口,是因为它一般不固定分配给某进程,而是动态分配的。

注意:我们自己开发的程序一般选择使用注册端口,且一个设备中不能出现两个程序的端口号一样,否则出错。

1.3 协议

网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

为了让世界上各种上网设备能够互联互通,肯定需要有一个组织出来,制定一个规则,大家都遵守这个规则,才能进行数据通信。

开放式网络互联标准:OSI网络参考模型

  • OSI网络参考模型:全球网络互联标准。
  • TCP/IP网络模型:事实上的国际标准。

传输层的2个通信协议

UDP(User Datagram Protocol):用户数据报协议;

TCP(Transmission Control Protocol) :传输控制协议。

1、UDP协议
  • 特点:无连接、不可靠通信。

  • 不事先建立连接,数据按照包发,一包数据包含:自己的IP、程序端口,目的地IP、程序端口和数据(限制在64KB内)等。

  • 发送方不管对方是否在线,数据在中间丢失也不管,如果接收方收到数据也不返回确认,故是不可靠的 。

通信效率高!广泛运用在语音通话(如果丢帧,无非就是声音小点之类),视频直播(无非画面模糊点)

2、TCP协议
  • 特点:面向连接、可靠通信。
  • TCP的最终目的:要保证在不可靠的信道上实现可靠的传输。
  • TCP主要有三个步骤实现可靠传输:三次握手建立连接,传输数据进行确认,四次挥手断开连接。

TCP协议:三次握手建立可靠连接

可靠连接:确定通信双方,收发消息都是正常无问题的!(全双工)

因为通过三次握手可以确定,双方的收发都没问题。确保可以传输数据和以及数据的可靠性。

TCP协议:四次握手断开连接

目的:确保双方数据的收发都已经完成!

服务端在响应的时候,极有可能也同时发了一些数据给客户端,作为最后一次数据,客户端收到数据后,发出正式确认数据都收完,然后断开连接。

通信效率相对不高,广泛应用在网页、文件下载、支付等。

二、UDP通信

特点:无连接、不可靠通信。

不事先建立连接;发送端每次把要发送的数据(限制在64KB内)、接收端IP、等信息封装成一个数据包,发出去就不管了。

Java提供了一个java.net.DatagramSocket类来实现UDP通信。

  • DatagramSocket: 用于创建客户端、服务端
构造器 说明
public DatagramSocket() 创建客户端的Socket对象, 系统会随机分配一个端口号。
public DatagramSocket(int port) 创建服务端的Socket对象, 并指定端口号
方法 说明
public void send(DatagramPacket dp) 发送数据包
public void receive(DatagramPacket p) 使用数据包接收数据
  • DatagramPacket:创建数据包
构造器 说明
public DatagramPacket(byte[] buf, int length, InetAddress address, int port) 创建发出去的数据包对象
public DatagramPacket(byte[] buf, int length) 创建用来接收数据的数据包
方法 说明
public int getLength() 获取数据包,实际接收到的字节个数

客户端实现步骤:

java 复制代码
① 创建DatagramSocket对象(客户端对象)           扔韭菜的人
② 创建DatagramPacket对象封装需要发送的数据(数据包对象)          韭菜盘子
③ 使用DatagramSocket对象的send方法,传入DatagramPacket对象           开始抛出韭菜
④ 释放资源

客户端代码如下:

java 复制代码
package com.itheima.d2_upd1;

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

/**
 * 目标:完成UDP通信快速入门:实现1发1收。
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建客户端对象(发韭菜出去的人)
        DatagramSocket socket = new DatagramSocket(7777);

        // 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
       /* public DatagramPacket(byte buf[], int length,
             InetAddress address, int port)
               参数一:封装要发出去的数据。
               参数二:发送出去的数据大小(字节个数)
               参数三:服务端的IP地址(找到服务端主机)
               参数四:服务端程序的端口。
             */
        byte[] bytes = "我是快乐的客户端,我爱你abc".getBytes();
        DatagramPacket packet = new DatagramPacket(bytes, bytes.length
                , InetAddress.getLocalHost(),  6666);

        // 3、开始正式发送这个数据包的数据出去了
        socket.send(packet);

        System.out.println("客户端数据发送完毕~~~");
        socket.close(); // 释放资源!
    }
}

服务端实现步骤

java 复制代码
①创建DatagramSocket对象并指定端口(服务端对象)           接韭菜的人
②创建DatagramPacket对象接收数据(数据包对象)         韭菜盘子
③使用DatagramSocket对象的receive方法,传入DatagramPacket对象          开始接收韭菜
④释放资源

服务端代码如下:

java 复制代码
package com.itheima.d2_upd1;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 目标:完成UDP通信快速入门-服务端开发
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("----服务端启动----");
        // 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
        DatagramSocket socket = new DatagramSocket(6666);

        // 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
        byte[] buffer = new byte[1024 * 64]; // 64KB.
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        // 3、开始正式使用数据包来接收客户端发来的数据
        socket.receive(packet);

        // 4、从字节数组中,把接收到的数据直接打印出来
        // 接收多少就倒出多少
        // 获取本次数据包接收了多少数据。
        int len = packet.getLength();

        String rs = new String(buffer, 0 , len);
        System.out.println(rs);

        System.out.println(packet.getAddress().getHostAddress());
        System.out.println(packet.getPort());

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

启动时,先启动服务端,然后启动客户端,才能保证数据不丢失

三、UDP通信-多发多收

通过循环,实现多发多收

客户端实现步骤

java 复制代码
① 创建DatagramSocket对象(发送端对象)           扔韭菜的人
② 使用while死循环不断的接收用户的数据输入,如果用户输入的exit则退出程序
③ 如果用户输入的不是exit, 把数据封装成DatagramPacket           韭菜盘子
④ 使用DatagramSocket对象的send方法将数据包对象进行发送          开始抛出韭菜
⑤ 释放资源
java 复制代码
package com.itheima.d3_udp2;

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

/**
 * 目标:完成UDP通信快速入门:实现客户端反复的发。
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建客户端对象(发韭菜出去的人)
        DatagramSocket socket = new DatagramSocket();

        // 2、创建数据包对象封装要发出去的数据(创建一个韭菜盘子)
       /* public DatagramPacket(byte buf[], int length,
             InetAddress address, int port)
               参数一:封装要发出去的数据。
               参数二:发送出去的数据大小(字节个数)
               参数三:服务端的IP地址(找到服务端主机)
               参数四:服务端程序的端口。
             */
        Scanner sc = new Scanner(System.in);
        while (true) {
            System.out.println("请说:");
            String msg = sc.nextLine();

            // 一旦发现用户输入的exit命令,就退出客户端
            if("exit".equals(msg)){
                System.out.println("欢迎下次光临!退出成功!");
                socket.close(); // 释放资源
                break; // 跳出死循环
            }

            byte[] bytes = msg.getBytes();
            DatagramPacket packet = new DatagramPacket(bytes, bytes.length
                    , InetAddress.getLocalHost(),  6666);

            // 3、开始正式发送这个数据包的数据出去了
            socket.send(packet);
        }
    }
}

接收端实现步骤

java 复制代码
①创建DatagramSocket对象并指定端口(接收端对象)           接韭菜的人

②创建DatagramPacket对象接收数据(数据包对象)          韭菜盘子

③使用DatagramSocket对象的receive方法传入DatagramPacket对象

④使用while死循环不断的进行第3步     
java 复制代码
package com.itheima.d3_udp2;

import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 目标:完成UDP通信快速入门-服务端反复的收
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("----服务端启动----");
        // 1、创建一个服务端对象(创建一个接韭菜的人) 注册端口
        DatagramSocket socket = new DatagramSocket(6666);

        // 2、创建一个数据包对象,用于接收数据的(创建一个韭菜盘子)
        byte[] buffer = new byte[1024 * 64]; // 64KB.
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);

        while (true) {
            // 3、开始正式使用数据包来接收客户端(们)发来的数据
            socket.receive(packet);

            // 4、从字节数组中,把接收到的数据直接打印出来
            // 接收多少就倒出多少
            // 获取本次数据包接收了多少数据。
            int len = packet.getLength();

            String rs = new String(buffer, 0 , len);
            System.out.println(rs);

            System.out.println(packet.getAddress().getHostAddress());
            System.out.println(packet.getPort());
            System.out.println("--------------------------------------");
        }
    }
}

服务器端不用释放资源,因为它是常开的

允许多开配置:

四、TCP通信

特点:面向连接、可靠通信。

通信双方事先会采用"三次握手"方式建立可靠连接,实现端到端的通信;底层能保证数据成功传给服务端。

Java提供了一个java.net.Socket类来实现TCP通信。

TCP通信之-客户端开发

客户端程序就是通过java.net包下的Socket类来实现的。

构造器 说明
public Socket(String host, int port) 根据指定的服务器ip、端口号请求与服务器端建立连接,链接通过,就获得了客户端socket
方法 说明
public OutputStream getOutputStream() 获得字节输出流对象
public InputStream getInputStream() 获得字节输入流对象

客户端实现步骤

①创建客户端的Socket对象,请求与服务端的连接。

②使用socket对象调用getOutputStream()方法得到字节输出流。

③使用字节输出流完成数据的发送。

④释放资源:关闭socket管道。

java 复制代码
package com.itheima.d4_tcp1;

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

/**
 *  目标:完成TCP通信快速入门-客户端开发:实现1发1收。
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1", 8888);

        // 2、从socket通信管道中得到一个字节输出流,用来发数据给服务端程序。
        OutputStream os = socket.getOutputStream();

        // 3、把低级的字节输出流包装成数据输出流
        DataOutputStream dos = new DataOutputStream(os);

        // 4、开始写数据出去了
        dos.writeUTF("在一起,好吗?");
        dos.close();

        socket.close(); // 释放连接资源
    }
}

TCP通信-服务端程序的开发

服务端是通过java.net包下的ServerSocket类来实现的。

构造器 说明
public ServerSocket(int port) 为服务端程序注册端口
方法 说明
public Socket accept() 阻塞等待客户端的连接请求,一旦与某个客户端成功连接,则返回服务端这边的Socket对象。

服务端实现步骤

①创建ServerSocket对象,注册服务端端口。

②调用ServerSocket对象的accept()方法,等待客户端的连接,并得到Socket管道对象。

③通过Socket对象调用getInputStream()方法得到字节输入流、完成数据的接收。

④释放资源:关闭socket管道

java 复制代码
package com.itheima.d4_tcp1;

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

/**
 *  目标:完成TCP通信快速入门-服务端开发:实现1发1收。
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8888);

        // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
        Socket socket = serverSocket.accept();

        // 3、从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();

        // 4、把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);

        // 5、使用数据输入流读取客户端发送过来的消息
        String rs = dis.readUTF();
        System.out.println(rs);
        // 其实我们也可以获取客户端的IP地址
        System.out.println(socket.getRemoteSocketAddress());

        dis.close();
        socket.close();
    }
}

五、TCP通信-多发多收

使用TCP通信实现:多发多收消息。

①客户端使用死循环,让用户不断输入消息。

②服务端也使用死循环,控制服务端收完消息,继续等待接收下一个消息。

客户端代码如下:

java 复制代码
package com.itheima.d5_tcp2;

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

/**
 *  目标:完成TCP通信快速入门-客户端开发:实现客户端可以反复的发消息出去
 */
public class Client {
    public static void main(String[] args) throws Exception {
        // 1、创建Socket对象,并同时请求与服务端程序的连接。
        Socket socket = new Socket("127.0.0.1", 8888);

        // 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();

            // 一旦用户输入了exit,就退出客户端程序
            if("exit".equals(msg)){
                System.out.println("欢迎您下次光临!退出成功!");
                dos.close();
                socket.close();
                break;
            }

            // 4、开始写数据出去了
            dos.writeUTF(msg);
            dos.flush();
        }
    }
}

服务端代码如下:

java 复制代码
package com.itheima.d5_tcp2;

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

/**
 *  目标:完成TCP通信快速入门-服务端开发:实现服务端反复发消息
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8888);

        // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
        Socket socket = serverSocket.accept();

        // 3、从socket通信管道中得到一个字节输入流。
        InputStream is = socket.getInputStream();

        // 4、把原始的字节输入流包装成数据输入流
        DataInputStream dis = new DataInputStream(is);

        while (true) {
            try {
                // 5、使用数据输入流读取客户端发送过来的消息
                String rs = dis.readUTF();
                System.out.println(rs);
            } catch (Exception e) {
                System.out.println(socket.getRemoteSocketAddress() + "离线了!");
                dis.close();
                socket.close();
                break;
            }
        }
    }
}

六、TCP通信-同时接收多个客户端

目前我们开发的服务端程序,是否可以支持与多个客户端同时通信 ?

不可以的。因为服务端现在只有一个主线程,只能处理一个客户端的消息。

为了让服务端能够支持多个客户端通信,就需要用到多线程技术。具体的实现思路如下图所示:每当有一个客户端连接服务端,在服务端这边就为Socket开启一条线程取执行读取数据的操作,来多少个客户端,就有多少条线程。用主线程接收客户端连接。按照这样的设计,服务端就可以支持多个客户端连接了。

按照这个思路,改写服务端的代码:

java 复制代码
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);

                } catch (Exception e) {
                    System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接下来,再改写服务端的主程序代码,如下:

java 复制代码
/**
 *  目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8888);

        while (true) {
            // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

            // 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
            new ServerReaderThread(socket).start();
        }
    }
}

七、TCP通信-综合案例(群聊)

把刚才的案例,改进成全能够实现群聊的效果,就是一个客户端发的消息,其他的每一个客户端都可以收到。

刚才我们写的多个客户端可以往服务端发现消息,但是客户端和客户端是不能直接通信的。想要试下全群聊的效果,我们还是必须要有服务端在中间做中转。 具体实现方案如下图所示:

我们可以在服务端创建一个存储Socket的集合,每当一个客户端连接服务端,就可以把客户端Socket存储起来;当一个客户端给服务端发消息时,再遍历集合通过每个Socket将消息再转发给其他客户端。

下面我们改造服务端代码,由于服务端读取数据是在线程类中完成的,所以我们改SerReaderThread类就可以了。服务端的主程序不用改。

java 复制代码
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        try {
            InputStream is = socket.getInputStream();
            DataInputStream dis = new DataInputStream(is);
            while (true){
                try {
                    String msg = dis.readUTF();
                    System.out.println(msg);
                    // 把这个消息分发给全部客户端进行接收。
                    sendMsgToAll(msg);
                } catch (Exception e) {
                    System.out.println("有人下线了:" + socket.getRemoteSocketAddress());
                    Server.onLineSockets.remove(socket);
                    dis.close();
                    socket.close();
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void sendMsgToAll(String msg) throws IOException {
        // 发送给全部在线的socket管道接收。
        for (Socket onLineSocket : Server.onLineSockets) {
            OutputStream os = onLineSocket.getOutputStream();
            DataOutputStream dos = new DataOutputStream(os);
            dos.writeUTF(msg);
            dos.flush();
        }
    }
}

八、BS架构程序(简易版)

前面我们所写的代码都是基于CS架构的。我们说网络编程还可以编写BS架构的程序,为了让同学们体验一下BS架构通信,这里我们写一个简易版的程序。仅仅只是体验下一,后期我们会详细学习BS架构的程序如何编写。

BS架构程序的实现原理,如下图所示:不需要开发客户端程序,此时浏览器就相当于是客户端,此时我们只需要写服务端程序就可以了。

在BS结构的程序中,浏览器和服务器通信是基于HTTP协议来完成的,浏览器给客户端发送数据需要按照HTTP协议规定好的数据格式发给服务端,服务端返回数据时也需要按照HTTP协议规定好的数据给是发给浏览器,只有这两双方才能完成一次数据交互。

客户端程序不需要我们编写(浏览器就是),所以我们只需要写服务端就可以了。

服务端给客户端响应数据的数据格式(HTTP协议规定数据格式)如下图所示:左图是数据格式,右图是示例。

接下来,我们写一个服务端程序按照右图示例的样子,给浏览器返回数据。注意:数据是由多行组成的,必须按照规定的格式来写。

8.1 服务端程序

先写一个线程类,用于按照HTTP协议的格式返回数据

java 复制代码
public class ServerReaderThread extends Thread{
    private Socket socket;
    public ServerReaderThread(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        //  立即响应一个网页内容:"黑马程序员"给浏览器展示。
        try {
            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);
            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:120px;text-align:center'>黑马程序员666<div>");
            ps.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再写服务端的主程序

java 复制代码
/**
 *  目标:完成TCP通信快速入门-服务端开发:要求实现与多个客户端同时通信。
 */
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8080);

        while (true) {
            // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
            Socket socket = serverSocket.accept();

            System.out.println("有人上线了:" + socket.getRemoteSocketAddress());

            // 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
            new ServerReaderThread(socket).start();
        }
    }
}

8.2 服务端主程序用线程池改进

为了避免服务端创建太多的线程,可以把服务端用线程池改进,提高服务端的性能。

先写一个给浏览器响应数据的线程任务

java 复制代码
public class ServerReaderRunnable implements Runnable{
    private Socket socket;
    public ServerReaderRunnable(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run() {
        //  立即响应一个网页内容:"黑马程序员"给浏览器展示。
        try {
            OutputStream os = socket.getOutputStream();
            PrintStream ps = new PrintStream(os);
            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:120px;text-align:center'>黑马程序员666<div>");
            ps.close();
            socket.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再改写服务端的主程序,使用ThreadPoolExecutor创建一个线程池,每次接收到一个Socket就往线程池中提交任务就行。

java 复制代码
public class Server {
    public static void main(String[] args) throws Exception {
        System.out.println("-----服务端启动成功-------");
        // 1、创建ServerSocket的对象,同时为服务端注册端口。
        ServerSocket serverSocket = new ServerSocket(8080);

        // 创建出一个线程池,负责处理通信管道的任务。
        ThreadPoolExecutor pool = new ThreadPoolExecutor(16 * 2, 16 * 2, 0, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(8) , Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        while (true) {
            // 2、使用serverSocket对象,调用一个accept方法,等待客户端的连接请求
            Socket socket = serverSocket.accept();

            // 3、把这个客户端对应的socket通信管道,交给一个独立的线程负责处理。
            pool.execute(new ServerReaderRunnable(socket));
        }
    }
}
相关推荐
重整旗鼓~几秒前
28.redisson源码分析分布式锁
java·开发语言
Query*1 分钟前
Java 设计模式——工厂模式:从原理到实战的系统指南
java·python·设计模式
懒羊羊不懒@32 分钟前
Java基础语法—最小单位、及注释
java·c语言·开发语言·数据结构·学习·算法
ss27335 分钟前
手写Spring第4弹: Spring框架进化论:15年技术变迁:从XML配置到响应式编程的演进之路
xml·java·开发语言·后端·spring
DokiDoki之父1 小时前
MyBatis—增删查改操作
java·spring boot·mybatis
兩尛1 小时前
Spring面试
java·spring·面试
Java中文社群1 小时前
服务器被攻击!原因竟然是他?真没想到...
java·后端
Full Stack Developme1 小时前
java.nio 包详解
java·python·nio
零千叶2 小时前
【面试】Java JVM 调优面试手册
java·开发语言·jvm
代码充电宝2 小时前
LeetCode 算法题【简单】290. 单词规律
java·算法·leetcode·职场和发展·哈希表