网络编程2


网络编程基础知识

计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。对某个特定的计算机网络来说,它可能使用网络协议 ABC,而另一个计算机网络可能使用网络协议 XYZ。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用 TCP/IP 协议。
TCP/IP 协议泛指互联网协议,其中最重要的两个协议是 TCP 协议和 IP 协议。只有使用 TCP/IP 协议的计算机才能够联入互联网,使用其他网络协议(例如 NetBIOS、AppleTalk 协议等)是无法联入互联网的。

网络编程三要素

  • IP 地址

要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而 IP 地址就是这个标识号。也就是设备的标识

  • 端口

网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说 IP 地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识

  • 协议

通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有 UDP 协议和 TCP 协议

IP 地址

在互联网中,一个 IP 地址用于唯一标识一个网络接口(Network Interface)。一台联入互联网的计算机肯定有一个 IP 地址,但也可能有多个 IP 地址。IP 地址分为 IPv4 和 IPv6 两种。IPv4 采用 32 位地址,类似101.202.99.12,而 IPv6 采用 128 位地址,类似2001:0DA8:100A:0000:0000:1020:F2F3:1428。IPv4 地址总共有 232 个(大约 42 亿),而 IPv6 地址则总共有 2128 个(大约 340 万亿亿亿亿),IPv4 的地址目前已耗尽,而 IPv6 的地址是根本用不完的。IP 地址又分为公网 IP 地址和内网 IP 地址。公网 IP 地址可以直接被访问,内网 IP 地址只能在内网访问。内网 IP 地址类似于:

  • 192.168.x.x
  • 10.x.x.x

有一个特殊的 IP 地址,称之为本机地址,它总是127.0.0.1

IPv4 地址实际上是一个 32 位整数。例如:

106717964 = 0x65ca630c
          = 65  ca  63 0c
          = 101.202.99.12

如果一台计算机只有一个网卡,并且接入了网络,那么,它有一个本机地址127.0.0.1,还有一个 IP 地址,例如101.202.99.12,可以通过这个 IP 地址接入网络。

如果一台计算机有两块网卡,那么除了本机地址,它可以有两个 IP 地址,可以分别接入两个网络。通常连接两个网络的设备是路由器或者交换机,它至少有两个 IP 地址,分别接入不同的网络,让网络之间连接起来。

如果两台计算机位于同一个网络,那么他们之间可以直接通信,因为他们的 IP 地址前段是相同的,也就是网络号是相同的。网络号是 IP 地址通过子网掩码过滤后得到的。例如:

某台计算机的 IP 是101.202.99.2,子网掩码是255.255.255.0,那么计算该计算机的网络号是:

IP = 101.202.99.2
Mask = 255.255.255.0
Network = IP & Mask = 101.202.99.0

每台计算机都需要正确配置 IP 地址和子网掩码,根据这两个就可以计算网络号,如果两台计算机计算出的网络号相同,说明两台计算机在同一个网络,可以直接通信。如果两台计算机计算出的网络号不同,那么两台计算机不在同一个网络,不能直接通信,它们之间必须通过路由器或者交换机这样的网络设备间接通信,我们把这种设备称为网关。

网关的作用就是连接多个网络,负责把来自一个网络的数据包发到另一个网络,这个过程叫路由。

所以,一台计算机的一个网卡会有 3 个关键配置:

  • IP 地址,例如:10.0.2.15
  • 子网掩码,例如:255.255.255.0
  • 网关的 IP 地址,例如:10.0.2.2

IP 地址:是网络中设备的唯一标识
IP 地址分为两大类

  • IPv4:是给每个连接在网络上的主机分配一个 32bit 地址。按照 TCP/IP 规定,IP 地址用二进制来表示,每个 IP 地址长 32bit,也就是 4 个字节。例如一个采用二进制形式的 IP 地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP 地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的 IP 地址可以表示为“192.168.1.66”。IP 地址的这种表示法叫做“点分十进制表示法”,这显然比 1 和 0 容易记忆得多;
  • IPv6:由于互联网的蓬勃发展,IP 地址的需求量愈来愈大,但是网络地址资源有限,使得 IP 的分配越发紧张。为了扩大地址空间,通过 IPv6 重新定义地址空间,采用 128 位地址长度,每 16 个字节一组,分成 8 组十六进制数,这样就解决了网络地址资源数量不够的问题;

常用命令:

ipconfig:查看本机 IP 地址

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

特殊 IP 地址: 127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用

域名

因为直接记忆 IP 地址非常困难,所以我们通常使用域名访问某个特定的服务。域名解析服务器 DNS 负责把域名翻译成对应的 IP,客户端再根据 IP 地址访问服务器。

nslookup可以查看域名对应的 IP 地址:

$ nslookup www.liaoxuefeng.com
Server:  xxx.xxx.xxx.xxx
Address: xxx.xxx.xxx.xxx#53
Non-authoritative answer:
Name:    www.liaoxuefeng.com
Address: 47.98.33.223

有一个特殊的本机域名localhost,它对应的 IP 地址总是本机地址127.0.0.1

端口

端口:设备上应用程序的唯一标识

端口号:用两个字节表示的整数,它的取值范围是 01023 之间的端口号用于一些知名的网络服务和应用(如浏览器的端口是 80),普通的应用程序需要使用 1024 以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败;

协议

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

协议的种类非常多,我们常见的有 UDP 协议和 TCP 协议

  • UDP 协议

用户数据报协议(User Datagram Protocol) UDP 是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。     由于使用 UDP 协议消耗资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输 例如视频会议通常采用 UDP 协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用 UDP 协议传送数据时,由于 UDP 的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用 UDP 协议

  • TCP 协议

传输控制协议 (Transmission Control Protocol) TCP 协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在 TCP 连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”   三次握手:TCP 协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠
第一次握手,客户端向服务器端发出连接请求,等待服务器确认;
第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求;
第三次握手,客户端再次向服务器端发送确认信息,确认连接;

网络模型

由于计算机网络从底层的传输到高层的软件设计十分复杂,要合理地设计计算机网络模型,必须采用分层模型,每一层负责处理自己的操作。OSI(Open System Interconnect 开放式系统互联)网络模型是 ISO 组织定义的一个计算机互联的标准模型,注意它只是一个定义,目的是为了简化网络各层的操作,提供标准接口便于实现和护。这个模型从上到下依次是:

  • 应用层,提供应用程序之间的通信;
  • 表示层:处理数据格式,加解密等等;
  • 会话层:负责建立和维护会话;
  • 传输层:负责提供端到端的可靠传输;
  • 网络层:负责根据目标地址选择路由来传输数据;
  • 链路层和物理层负责把数据进行分片并且真正通过物理网络传输,例如,无线网、光纤等。

互联网实际使用的 TCP/IP 模型并不是对应到 OSI 的 7 层模型,而是大致对应 OSI 的 5 层模型:

  • TCP(Transmission Control Protocol)传输控制协议
    传输基本单位是报文段

应用范围:运输层,进程之间

  • UDP(User Datagram Protocol)用户数据报协议
    传输基本单位是用户数据报

应用范围:运输层,不同主机上的进程之间

  • IP(Internet Protocol)协议
    用于网络层,主要为不同的主机之间的数据传输提供服务

服务与协议之间的关系

协议是对等的,服务是垂直的。

协议的作用是保证不同实体之间的传输是对等的,而不同层之间,通过“服务”来使下层给上层暴露出有限的接口,即层间接口,使得上层能够使用下层的服务,来完成协议的功能

参考:https://www.cnblogs.com/bqwzx/p/11053778.html

小结

计算机网络的基本概念主要有:

  • 计算机网络:由两台或更多计算机组成的网络;
  • 互联网:连接网络的网络;
  • IP 地址:计算机的网络接口(通常是网卡)在网络中的唯一标识;
  • 网关:负责连接多个网络,并在多个网络之间转发数据的计算机,通常是路由器或交换机;
  • 网络协议:互联网使用 TCP/IP 协议,它泛指互联网协议簇;
  • IP 协议:一种分组交换传输协议;
  • TCP 协议:一种面向连接,可靠传输的协议;
  • UDP 协议:一种无连接,不可靠传输的协议。

UDP 通信

InetAddress 的使用

为了方便我们对 IP 地址的获取和操作,Java 提供了一个类 InetAddress 供我们使用
InetAddress:此类表示 Internet 协议(IP)地址

方法名 说明
static InetAddress getByName(String host) 确定主机名称的 IP 地址。主机名称可以是机器名称,也可以是 IP 地址
String getHostName() 获取此 IP 地址的主机名
String getHostAddress() 返回文本显示中的 IP 地址字符串
InetAddress address = InetAddress.getByName("A23451413-2");
String name = address.getHostName();
String ip = address.getHostAddress();
System.out.println("主机名:"+name);    //主机名:A23451413-2
System.out.println("端口:"+ip);   //端口:10.17.50.62

UDP 通信原理

UDP 协议是一种不可靠的网络协议,它在通信的两端各建立一个 Socket 对象,但是这两个 Socket 只是发送,接收数据的对象 因此对于基于 UDP 协议的通信双方而言,没有所谓的客户端和服务器的概念 Java 提供了DatagramSocket 类作为基于 UDP 协议的 Socket;

DatagramSocket 构造方法:

构造器 描述
DatagramSocket() 构造一个数据报套接字并将其绑定到本地主机上的任何可用端口。
DatagramSocket(int port) 构造一个数据报套接字并将其绑定到本地主机上的指定端口。
DatagramSocket(int port,InetAddress laddr) 创建绑定到指定本地地址的数据报套接字。

常用方法:

变量和类型 方法 描述
void bind(SocketAddress addr) 将此 DatagramSocket 绑定到特定的地址和端口。
void close() 关闭此数据报套接字。
void receive(DatagramPacket p) 从此套接字接收数据报包。
void send(DatagramPacket p) 从此套接字接收数据报包。

DatagramPacket构造方法:

//接收数据
DatagramPacket(byte[] buf, int length)
    //构造一个 DatagramPacket用于接收长度的数据包 length 。
DatagramPacket(byte[] buf, int offset, int length)
    //构造一个 DatagramPacket用于接收长度的分组 length ,指定偏移到缓冲器中。

//发送数据
DatagramPacket(byte[] buf, int length, InetAddress address, int port)
    //构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
    //构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
    //构造用于发送长度的分组数据报包 length具有偏移 ioffset指定主机上到指定的端口号。
DatagramPacket(byte[] buf, int length, SocketAddress address)
    //构造用于发送长度的分组的数据报包 length指定主机上到指定的端口号。

DatagramPacket实例对象的方法:

变量和类型 方法 描述
InetAddress getAddress() 返回发送此数据报或从中接收数据报的计算机的 IP 地址。
byte[] getData() 返回数据缓冲区。
int getLength() 返回要发送的数据的长度或接收的数据的长度。
void setData(byte[] buf) 设置此数据包的数据缓冲区。
void setData(byte[] buf, int offset, int length) 设置此数据包的数据缓冲区。

UDP 通信程序** **

//1.创建数据报套接字
DatagramSocket ds = new DatagramSocket();

//2.字符数组
byte[] bts = "hello java 我爱你".getBytes();

//3.数据报包
//DatagramPacket(byte[] buf, int length, InetAddress address, int port)
DatagramPacket dp = new DatagramPacket(bts,bts.length,InetAddress.getByName("10.17.50.62"),8787);

//4.发送
ds.send(dp);

//5.关闭套接字
ds.close();

//1.创建接收端的Socket对象(DatagramSocket)
DatagramSocket ds = new DatagramSocket(8787);

//2.创建一个数据包,用于接收数据
byte[] bts = new byte[1024];
DatagramPacket dp = new DatagramPacket(bts, bts.length);

//3.调用DatagramSocket对象的receive方法接收数据
ds.receive(dp);

//4.解析数据包,并把数据在控制台显示
byte[] datas = dp.getData();
//int getLength() 返回要发送的数据的长度或接收的数据的长度。
String s = new String(datas, 0, dp.getLength());
System.out.println("接收到数据是:"+s);

//5.关闭接收端
ds.close();

练习:

发送:

//1.创建数据报套接字
DatagramSocket ds = new DatagramSocket();

Scanner sc = new Scanner(System.in);
String line;
while ((line = sc.nextLine())!=null){
    if(line.equals("886")){
        break;
    }
    byte[] bts = line.getBytes();
    DatagramPacket dp = new DatagramPacket(bts,bts.length,InetAddress.getByName("10.17.50.62"),8989);

    ds.send(dp);
}

ds.close();

接收:

DatagramSocket ds = new DatagramSocket(8989);

while (true){
    byte[] bts = new byte[1024];
    DatagramPacket dp = new DatagramPacket(bts, bts.length);

    ds.receive(dp);

    String s = new String(dp.getData(), 0, dp.getLength());
    System.out.println("接收到数据是:"+s);
}
//ds.close();   //服务器一般不关闭

TCP 通信

TCP 通信原理

TCP 通信协议是一种可靠的网络协议,它在通信的两端各建立一个 Socket 对象,从而在通信的两端形成网络虚拟链路, 一旦建立了虚拟的网络链路,两端的程序就可以通过虚拟链路进行通信,通信的两端,要严格区分为客户端(Client)与服务端(Server)。

C/S 通信时步骤:

  1. 服务端程序,需要事先启动,等待客户端的连接。
  2. 客户端主动连接服务器端,连接成功才能通信。服务端不可以主动连接客户端。

Java 提供了两个类用于实现 TCP 通信程序:

  • 客户端:java.net.Socket 类表示。创建Socket对象,向服务端发出连接请求,服务端响应请求,两者建立连接开始通信。
  • 服务端:java.net.ServerSocket 类表示。创建ServerSocket对象,相当于开启一个服务,并等待客户端的连接。

Socket 类

该类实现客户端套接字。 套接字是两台机器之间通信的端点。

构造方法

  • public Socket(String host, int port)

创建套接字对象并将其连接到指定主机 host 上的指定端口号 port。
如果指定的 host 是 null ,则相当于指定地址为回送地址。
小贴士:回送地址(127.x.x.x) 是本机回送地址(Loopback Address),主要用于网络软件测试以及本地机进程间通信,无论什么程序,一旦使用回送地址发送数据,立即返回,不进行任何网络传输。

构造举例,代码如下:

Socket client = new Socket("127.0.0.1", 6666);

成员方法

  • public InputStream getInputStream()

返回此套接字的输入流。
InputStream is = s.getInputStream();
byte[] bys = new byte[1024];
int len = is.read(bys);
String data = new String(bys, 0, len);
System.out.println(“接收到客户端数据:”+data);

  • public OutputStream getOutputStream()

返回此套接字的输出流。
OutputStream os = s.getOutputStream();
os.write(“我收到你的消息了客户端”.getBytes());

  • public void close()

关闭此套接字。

  • public void shutdownOutput()

禁用此套接字的输出流。任何先前写出的数据将被发送,随后终止输出流。

ServerSocket 类

这个类实现了服务器套接字,该对象等待通过网络的请求。

构造方法

  • public ServerSocket(int port)

    使用该构造方法在创建 ServerSocket 对象时,可以绑定到一个指定的端口号,参数 port 就是端口号。

构造举例,代码如下:

ServerSocket server = new ServerSocket(6666);

成员方法

  • public Socket accept() throws IOException

    侦听并接受连接,返回一个新的 Socket 对象,用于和客户端实现通信。该方法会一直阻塞直到客户端请求建立连接。

TCP 通信程序

服务端:

public class ServerTcp {
    public static void main(String[] args) throws IOException {
        //1.创建服务端对象
        ServerSocket ss = new ServerSocket(7788);

        //2.监听客户端连接,并获得套接字对象
        System.out.println("等待客户端连接>>>");
        Socket s = ss.accept();

        //3.获得输入流,读数据,并把数据显示在控制台
        InputStream is = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = is.read(bys);
        String data = new String(bys,0,len);
        System.out.println("服务端接收到的数据是:"+data);

        //3.获得输出流向客户端发送数据
        OutputStream outputStream = s.getOutputStream();
        outputStream.write(data.toUpperCase().getBytes());

        //4.释放资源
        s.close();  //可以不用写
        ss.close();
    }
}

客户端:

public class ClientTcp {
    public static void main(String[] args) throws IOException {
        //1.创建socket对象
        Socket s = new Socket("127.0.0.1",7788);

        //2.获得输出流,写数据
        OutputStream outputStream = s.getOutputStream();
        outputStream.write("我是tcp客户端,我非常喜欢java编程".getBytes());

        //3.获得输入流,接收数据
        InputStream inputStream = s.getInputStream();
        byte[] bys = new byte[1024];
        int len = inputStream.read(bys);
        System.out.println("收到服务端的回复:"+new String(bys,0,len));

        //4.释放资源
        s.close();
    }
}

TCP 案例练习

练习 1

  • 客户端:发送数据,接收服务端反馈
  • 服务端:接收数据,给出反馈

练习 2

  • 客户端:数据来自键盘录入,直到输入的数据是 886,发送数据结束
  • 服务端:接收到的数据在控制台输出

练习 3

  • 客户端:数据来自键盘录入,直到输入的数据是 886,发送数据结束
  • 服务端:接收到的数据写入文本文件

服务端:

public class ServerDemo1 {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(8899);

        Socket s = ss.accept();

        BufferedReader br = new BufferedReader(new InputStreamReader(s.getInputStream()));
        BufferedWriter bw = new BufferedWriter(new FileWriter("F:\\aaa.txt"));

        String line;
        while ((line = br.readLine())!=null){
            bw.write(line);
            bw.newLine();
            bw.flush();
        }
		bw.close();
        ss.close();
    }
}

客户端:

public class ClientDemo1 {
    public static void main(String[] args) throws IOException {
        Socket s = new Socket("10.17.50.62",8899);
        //数据来自键盘录入,方式一:用BufferedReader封装
        //        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

        //方式二:用Scanner
        Scanner sc = new Scanner(System.in);

        //数据封装到输出流
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(s.getOutputStream()));

        /*String line;
        while ((line = br.readLine())!=null && !line.equals("886")){
            bw.write(line);
            bw.newLine();
            bw.flush();
        }*/

        while (true){
            String s1 = sc.nextLine();
            if(s1.equals("886")){
                break;
            }
            bw.write(s1);
            bw.newLine();
            bw.flush();
        }
        s.close();
    }
}

练习 4(上传文件)

  • 客户端:数据来自文本文件
  • 服务端:接收到的数据写入文本文件

文件上传分析图解:

  1. 【客户端】输入流,从硬盘读取文件数据到程序中。
  2. 【客户端】输出流,写出文件数据到服务端。
  3. 【服务端】输入流,读取文件数据到服务端程序。
  4. 【服务端】输出流,写出文件数据到服务器硬盘中。

客户端实现:

public class UploadClient {
    public static void main(String[] args) throws IOException {
        //1.
        Socket sock = new Socket("127.0.0.1",7788);
        //2.准备数据源
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream("./1.jpg"));
        //3.输出流
        BufferedOutputStream bos = new BufferedOutputStream(sock.getOutputStream());
        byte[] bys = new byte[1024];
        int len=0;
        while ((len = bis.read(bys))!= -1){
            bos.write(bys,0,len);
            bos.flush();
        }
        //注意:上传成功如果没有结束标记,可以使用:shutdownOutput()  禁用此套接字的输出流。
        System.out.println("文件上传成功!");
        //4.
        sock.close();
    }
}

服务器端实现:

public class UploadServer {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(7788);
        Socket s = ss.accept();
        //输入流-获取客户端发送的数据
        BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
        //输出流-保存文件到本地
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("./copy1.jpg"));
        //读写数据
        byte[] b = new byte[1024];
        int len = 0;
        while ((len = bis.read(b))!=-1){
            bos.write(b,0,len);
            bos.flush();
        }
        System.out.println("服务端收到文件");
        //关闭
        s.close();
        bis.close();    //记得要关闭文件
        ss.close();
    }
}

文件上传优化分析:

  1. 文件名称写死的问题
    服务端,保存文件的名称如果写死,那么最终导致服务器硬盘,只会保留一个文件,建议使用系统时间优化,保证文件名称唯一,代码如下:
//准备文件夹
File f1 = new File("./upload");
if(!(f1.exists())){
f1.mkdirs();
}
//不重复的文件名
String filename = "xjt"+System.currentTimeMillis()+ new Random().nextInt(100)+".mkv";
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+"//"+filename));
  1. 循环接收的问题
    服务端,指保存一个文件就关闭了,之后的用户无法再上传,这是不符合实际的,使用循环改进,可以不断的接收不同用户的文件,代码如下:
// 每次接收新的连接,创建一个Socket
whiletrue{
    Socket accept = serverSocket.accept();
    ......
}
  1. 多线程提高效率
    服务端,在接收大文件时,可能耗费几秒钟的时间,此时不能接收其他用户上传,所以,使用多线程技术优化,代码如下:
while(true){
    Socket accept = serverSocket.accept();
    // accept 交给子线程处理.
    new Thread(() -> {
        ......
        InputStream bis = accept.getInputStream();
        ......
    }).start();
}

优化实现-服务端

public class UploadServer {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(7788);
        while(true){
            Socket s = ss.accept();
            //线程池
            ExecutorService ec = Executors.newFixedThreadPool(10);
            ec.submit(() ->{
                try{
                    //输入流-获取客户端发送的数据
                    BufferedInputStream bis = new BufferedInputStream(s.getInputStream());
                    //输出流-保存文件到本地
                    File f1 = new File("./upload");
                    if(!(f1.exists())){
                        f1.mkdirs();
                    }
                    //文件名称不重复
                    String filename = "xjt"+System.currentTimeMillis()+ new Random().nextInt(100)+".mkv";
                    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(f1+"//"+filename));
                    //读写数据
                    byte[] b = new byte[1024];
                    int len = 0;
                    while ((len = bis.read(b))!=-1){
                        bos.write(b,0,len);
                        bos.flush();
                    }
                    System.out.println("服务端收到文件,当前线程是:"+Thread.currentThread().getName());
                    //关闭
                    s.close();
                    bis.close();    //记得要关闭文件
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
        }
        //ss.close();   //服务器一般不用关
    }
}

练习 5

  • 客户端:数据来自文本文件,接收服务端反馈
  • 服务端:接收到的数据写入文本文件,给出反馈

出现问题:程序一直等待
原因:读数据的方法是阻塞式的
解决办法:自定义结束标记,使用 shutdownOutput() 方法

练习 6

  • 客户端:数据来自文本文件,接收服务端反馈
  • 服务端:接收到的数据写入文本文件,给出反馈,代码用线程进行封装,为每一个客户端开启一个线程

练习 6

模拟网站服务器,使用浏览器访问自己编写的服务端程序,查看网页效果。

案例分析:

  1. 准备页面数据,web 文件夹。
    复制到我们 Module 中,比如复制到 day08 中
  2. 我们模拟服务器端,ServerSocket 类监听端口,使用浏览器访问
  3. 服务器程序中字节输入流可以读取到浏览器发来的请求信息

GET/web/index.html HTTP/1.1 是浏览器的请求消息。/web/index.html 为浏览器想要请求的服务器端的资源,使用字符串切割方式获取到请求的资源。

//转换流,读取浏览器请求第一行
BufferedReader readWb = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String requst = readWb.readLine();
//取出请求资源的路径
String[] strArr = requst.split(" ");
//去掉web前面的/
String path = strArr[1].substring(1);
System.out.println(path);

案例实现

服务端实现:

public class SerDemo {
    public static void main(String[] args) throws IOException {
        System.out.println("服务端  启动 , 等待连接 .... ");
        // 创建ServerSocket 对象
        ServerSocket server = new ServerSocket(8888);
        Socket socket = server.accept();
        // 转换流读取浏览器的请求消息
        BufferedReader readWb = new
        BufferedReader(new InputStreamReader(socket.getInputStream()));
        String requst = readWb.readLine();
        // 取出请求资源的路径
        String[] strArr = requst.split(" ");
        // 去掉web前面的/
        String path = strArr[1].substring(1);
        // 读取客户端请求的资源文件
        FileInputStream fis = new FileInputStream(path);
        byte[] bytes= new byte[1024];
        int len = 0 ;
        // 字节输出流,将文件写会客户端
        OutputStream out = socket.getOutputStream();
        // 写入HTTP协议响应头,固定写法
        out.write("HTTP/1.1 200 OK\r\n".getBytes());
        out.write("Content-Type:text/html\r\n".getBytes());
        // 必须要写入空行,否则浏览器不解析
        out.write("\r\n".getBytes());
        while((len = fis.read(bytes))!=-1){
            out.write(bytes,0,len);
        }
        fis.close();
        out.close();
        readWb.close();
        socket.close();
        server.close();
    }
}
public class ServerDemo {
    public static void main(String[] args) throws IOException {
        ServerSocket server = new ServerSocket(8888);
        while(true){
            Socket socket = server.accept();
            new Thread(new Web(socket)).start();
        }
    }
    static class Web implements Runnable{
        private Socket socket;
        public Web(Socket socket){
            this.socket=socket;
        }
        public void run() {
            try{
                //转换流,读取浏览器请求第一行
                BufferedReader readWb = new
                        BufferedReader(new InputStreamReader(socket.getInputStream()));
                String requst = readWb.readLine();
                //取出请求资源的路径
                String[] strArr = requst.split(" ");
                System.out.println(Arrays.toString(strArr));
                String path = strArr[1].substring(1);
                System.out.println(path);
                FileInputStream fis = new FileInputStream(path);
                System.out.println(fis);
                byte[] bytes= new byte[1024];
                int len = 0 ;
                //向浏览器 回写数据
                OutputStream out = socket.getOutputStream();
                out.write("HTTP/1.1 200 OK\r\n".getBytes());
                out.write("Content-Type:text/html\r\n".getBytes());
                out.write("\r\n".getBytes());
                while((len = fis.read(bytes))!=-1){
                    out.write(bytes,0,len);
                }
                fis.close();
                out.close();
                readWb.close();
                socket.close();
            }catch(Exception ex){
            }
        }
    }
}

文章作者: CoderXiong
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 CoderXiong !
  目录