跳到主要内容

第24章-网络编程简介【扩展】

概述

  • 现代的软件结构形态,都不再是由单个软件或服务构成,而是一组软件或一组服务组成统一提供服务
  • 由此,软件与软件之间如何通信,也是软件开发过程中非常重要的技术
  • 想要实现网络互联的不同计算机上运行的程序间可以进行数据交换,那么就要涉及到网络编程的“三要素”,这样就能解决以下的三个问题:
    1. 如何准确地定位网络上一台或多台主机?IP地址
    2. 如何定位到主机上某个特定的应用?端口号
    3. 找到主机后如何可靠高效地进行数据传输?通讯协议

网络编程三要素

IP地址

  • 要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识,简单说就是设备在网络中的地址,是唯一的标识
  • 目前,IP地址广泛使用的版本是IPv4,它是由4个字节大小的二进制数来表示,例如:0000-1010 0000-0000 0000-0000 0000-0001。由于二进制形式表示的IP地址非常不便记忆和处理,因此通常会将IP地址写成十进制的形式,每个字节(8位)用一个十进制整数来(0-255)表示,数字间用符号“.”分开,如“192.168.0.1”。因为8位对应的十进制无符号整数为:0-255,所以-4.278.4.1是错误的IPv4地址
  • 随着计算机网络规模的不断扩大,对IP地址的需求也越来越多,IPv4这种用4个字节表示的IP地址面临枯竭,因此IPv6便应运而生了,IPv6使用16个字节表示IP地址,它所拥有的地址容量达到2^128 个(算上全零的),这样就解决了网络地址资源数量不够的问题。16个字节写成8个16位的无符号整数,每个整数用四个十六进制位表示,每个数之间用冒号分开,如:3ffe: 3201:1401:1280:c8ff:fe4d:db39:1984。
  • IPv4一般习惯性分为4个8位表示,中间用.分开,比如192.168.1.1
  • IPv4基础概念
    • IP地址:192.168.72.129
    • 子网掩码:255.255.255.0
    • 对应网段:192.168.72.0,该网段可用主机为192.168.72.1~192.168.72.254,192.168.72.255为广播地址,不同网段是天然隔离的,不能相互访问的
    • 127.0.0.1:本机地址,也叫环回地址,对应域名是localhost
  • 为了解决网络地址不够的问题,IPv4中推出了企业中常见的私有网络IP地址,能在不同规模企业的网络中各自使用;如果需要访问互联网络,则可整个网络映射一个或多个公有网络IP地址
    • 10.0.0.0~10.255.255.255
    • 172.16.0.0~172.31.255.555
    • 192.168.0.0~192.168.255.255
  • 域名
    • IP地址不好记忆,就可以使用域名并绑定相应的IP地址
    • 域名-》IP地址的解析需要域名解析服务器DNS(Domain Name System)来处理
  • IP地址查看命令:ipconfig、ifconfig、ip addr等命令
域名和DNS
域名
  • IP地址毕竟是数字标识,使用时不好记忆和书写,因此在IP地址的基础上又发展出一种符号化的地址方案,来代替数字型的IP地址。每一个符号化的地址都与特定的IP地址对应。这个与网络上的数字型IP地址相对应的字符型地址,就被称为域名
  • 目前域名已经成为互联网品牌、网上商标保护必备的要素之一,除了识别功能外,还有引导、宣传等作用。如:www.baidu.com。
DNS
  • 在Internet上域名与IP地址之间是一对一(或者多对一)的,域名虽然便于人们记忆,但机器之间只能互相认识IP地址,它们之间的转换工作称为域名解析,域名解析需要由专门的域名解析服务器来完成,DNS(Domain Name System域名系统)就是进行域名解析的服务器,域名的最终指向是IP

端口

  • IP地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,如何来区分这些不同的程序呢?这就要用到端口

  • 在计算机中,不同的应用程序是通过端口号区分的。端口号是用两个字节(无符号)表示的,它的取值范围是0~65535,而这些计算机端口可分为3大类

    • 公认端口:0~1023。被预先定义的服务通信占用(如:HTTP占用端口80,FTP占用端口21,Telnet占用端口23等)
    • 注册端口:1024~49151。分配给用户进程或应用程序。(如:Tomcat占用端口8080,MySQL占用端口3306,Oracle占用端口1521等)
    • 动态/私有端口:49152~65535
  • IP地址好比每个人的地址(门牌号),端口好比是房间号。必须同时指定IP地址和端口号才能够正确的发送数据。接下来通过一个图例来描述IP地址和端口号的作用,如下图所示

    网络编程-三要素-端口号-1

    从上图中可以清楚地看到,位于网络中一台计算机可以通过IP地址去访问另一台计算机,并通过端口号访问目标计算机中的某个应用程序

通讯协议

概述
  • 通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则。就像两个人想要顺利沟通就必须使用同一种语言一样,如果一个人只懂英语而另外一个人只懂中文,这样就会造成没有共同语言而无法沟通
  • 在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换
OSI参考模型
  • 世界上第一个网络体系结构由IBM公司提出(1974年,SNA),以后其他公司也相继提出自己的网络体系结构如:Digital公司的DNA,美国国防部的TCP/IP等,多种网络体系结构并存,其结果是若采用IBM的结构,只能选用IBM的产品,只能与同种结构的网络互联

  • 为了促进计算机网络的发展,国际标准化组织ISO(International Organization for Standardization)于1977年成立了一个委员会,在现有网络的基础上,提出了不基于具体机型、操作系统或公司的网络体系结构,称为开放系统互连参考模型,即OSI/RM (Open System Interconnection Reference Model)

  • OSI模型把网络通信的工作分为7层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层

  • OSI七层协议模型如图所示

    网络编程-三要素-通讯协议-1

TCP/IP参考模型
  • OSI参考模型的初衷是提供全世界范围的计算机网络都要遵循的统一标准,但是由于存在模型和协议自身的缺陷,迟迟没有成熟的产品推出。TCP/IP协议在实践中不断完善和发展取得成功,作为网络的基础,Internet的语言,可以说没有TCP/IP协议就没有互联网的今天

  • TCP/IP,即Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议、Internet国际互联网络的基础

  • TCP/IP协议是一个开放的网络协议簇,它的名字主要取自最重要的网络层IP协议和传输层TCP协议。TCP/IP协议定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。TCP/IP参考模型采用4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求,这4个层次分别是:网络接口层、互联网层(IP层)、传输层(TCP层)、应用层

  • OSI模型与TCP/IP模型的对应关系如图所示

Socket-网络模型

注意:那么TCP/IP协议和OSI模型有什么区别呢?

OSI网络通信协议模型,是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI 模型,但是并没有严格按照OSI规定的七层标准去划分,而只划分了四层,这样会更简单点,当划分太多层次时,你很难区分某个协议是属于哪个层次的。TCP/IP协议和OSI模型也并不冲突,TCP/IP协议中的应用层协议,就对应于OSI中的应用层,表示层,会话层。TCP/IP中有两个重要的协议,传输层的TCP协议和互连网络层的IP协议,因此就拿这两个协议做代表,来命名整个协议族了,再说TCP/IP协议时,是指整个协议族

常用网络连通检测命令

  • ping,是一个常用命令,主要作用有
    • 检测网络连通情况和分析网络速度
    • 根据域名得到服务器IP地址 根据返回的TTL(Time To Live)值来判断对方所使用的操作系统及数据包经过路由器数量
    • 命令格式为:ping ip或ping 域名
  • telnet,一个应用层协议
    • 一般用于检测指定端口是否开放,有些服务器出于安全问题,有防火墙或通过策略限制了某些端口的访问
    • 命令格式为:telnet ip 端口
  • cURL,一个利用URL语法在命令行下工作工具,可以做HTTP请求,也可以传输文件
    • 一般需要安装,如CentOS下使用yum install curl
    • 查看部署的Web应用中是否可访问

MAC简介

  • MAC地址(Media Access Control Address),也称物理地址,使用48位二进制表示,如:00-2B-67-45-1F-EA
  • 在TCP/IP协议中处于数据链路层,是给网络中的每一台设备做唯一标识的,如每张网卡都有一个MAC地址
  • 区别于IP地址可以动态分配和绑定,MAC地址是出厂就烧入设备,一般不可修改
  • 网络中设备间的消息发送,是会传递自身的IP地址和MAC地址做为标识的
  • 可通过ipconfig /all查看主机网卡的MAC地址
  • MAC地址查看命令:ipconfig /all、ifconfig -a、ip addr show等命令

网络编程基础类

  • Java为了可移植性,不允许直接调用操作系统,而是由java.net包来提供网络功能。Java虚拟机负责提供与操作系统的实际连接。

InetAddress类

概述
  • java.net.IntAddress类用来封装计算机的IP地址和DNS(没有端口信息),它包括一个主机名和一个IP地址,是java对IP地址的高层表示。大多数其它网络类都要用到这个类,包括Socket、ServerSocket、URL、DatagramSocket、DatagramPacket等
常用静态方法
方法名描述
public static InetAddress getByName(String host)传入目标主机的名字或IP地址得到对应的InetAddress对象,其中封装了IP地址和主机名
public static InetAddress getLocalHost()得到本机的InetAddress对象,其中封装了IP地址和主机名

调用getByName(String host)方法,只需要传入目标主机的名字或IP地址,InetAddress会尝试做连接DNS服务器,并且获取IP地址和主机名的操作

常用成员方法
方法名描述
public String getHostAddress()获得IP地址
public String getHostName()获得主机名
应用实例
  • 应用实例1,获取本机信息

    package com.bjpowernode.demo;

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

    /**
    * 获取本机信息
    */
    public class InetAdressDemo {
    public static void main(String[] args) {
    try {
    // 获取本机的IP地址和主机名
    InetAddress ia = InetAddress.getLocalHost();
    // 得到本机的主机名
    System.out.println(ia.getHostName());
    // 得到本机的IP地址
    System.out.println(ia.getHostAddress());
    } catch (UnknownHostException e) {
    e.printStackTrace();
    }
    }
    }
  • 应用实例2,获取网络主机信息

    package com.bjpowernode.demo;

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

    /**
    * 获取网络主机信息
    */
    public class InetAdressDemo {
    public static void main(String[] args) {
    try {
    //根据百度域名获取主机信息
    InetAddress name = InetAddress.getByName("baidu.com");
    //主机名
    System.out.println(name.getHostName());
    //IP地址
    System.out.println(name.getHostAddress());
    } catch (UnknownHostException e) {
    e.printStackTrace();
    }
    }
    }

【练习】

  1. 练习应用实例内容,完成代码编写

URL类

概述
  • java.net.URL是统一资源定位符,对可以从互联网上得到的资源的位置和访问方法的一种简洁的表示,是互联网上标准资源的地址。互联网上的每个文件都有一个唯一的URL,它包含的信息指出文件的位置以及浏览器应该怎么处理它
  • URL由4部分组成:协议、存放资源的主机域名、资源文件名和端口号。如果未指定该端口号,则使用协议默认的端口。例如HTTP协议的默认端口为80。在浏览器中访问网页时,地址栏显示的地址就是URL
  • URL标准格式为:<协议>://<域名或IP>:<端口>/<路径>。其中,<协议>://<域名或IP>是必需的,<端口>/<路径> 有时可省略。如:https://www.baidu.com
  • 为了方便程序员编程,JDK中提供了URL类,该类的全名是java.net.URL,该类封装了大量复杂的涉及从远程站点获取信息的细节,可以使用它的各种方法来对URL对象进行分割、合并等处理
应用实例
  • 应用实例1,URL使用

    package com.bjpowernode.demo;

    import java.net.MalformedURLException;
    import java.net.URL;

    /**
    * URL实例
    */
    public class URLDemo {
    public static void main(String[] args) throws MalformedURLException {
    // 创建URL对象
    URL url = new URL("http://www.baidu.com:8080/java/index.html?name=admin#tip");
    // 获取协议,输出:http
    System.out.println("协议:" + url.getProtocol());
    // 获取域名,输出:www.jd.com
    System.out.println("域名:" + url.getHost());
    // 获取与URL关联的协议的默认端口,HTTP协议默认端口为80
    System.out.println("获取与URL关联的协议的默认端口:" + url.getDefaultPort());
    // 获取端口,不存在则返回-1,输出:8080
    System.out.println("端口:" + url.getPort());
    // 获取端口号后,参数前的内容,输出:/java/index.html
    System.out.println("getPath:" + url.getPath());
    // 获取端口号后的所有内容,不包含锚点,输出:/java/index.html?name=admin
    System.out.println("getFile:" + url.getFile());
    // 获取参数,输出:name=admin
    System.out.println("参数:" + url.getQuery());
    // 获取锚点,输出:tip
    System.out.println("锚点:" + url.getRef());
    }
    }
  • 实例2,使用URL获取网络数据

    package com.bjpowernode.demo;

    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.net.MalformedURLException;
    import java.net.URL;

    /**
    * 使用URL获取网络数据
    */
    public class URLDemo {
    public static void main(String[] args){
    BufferedReader br = null;
    try {
    //URL地址
    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();
    }
    }
    }
    }
    }

【练习】

  1. 练习应用实例内容,完成代码编写

TCP和UDP协议

Socket套接字的概述

  • 开发的网络应用程序位于应用层,TCP和UDP属于传输层协议,在应用层如何使用传输层的服务呢?在应用层和传输层之间,则是使用套接Socket来进行分离

  • 套接字就像是传输层为应用层开的一个小口,应用程序通过这个小口向远程发送数据,或者接收远程发来的数据。而这个小口以内,也就是数据进入这个口之后,或者数据从这个口出来之前,是不知道也不需要知道的,也不会关心它如何传输,这属于网络其它层次工作

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

  • 具体方式如下图

    网络编程-TCP和UDP协议-1

TCP和UDP协议

  • TCP协议和UDP协议是传输层的两种协议。Socket是传输层供给应用层的编程接口,所以Socket编程就分为TCP编程和UDP编程两类。
TCP协议
  • 使用TCP协议,须先建立TCP连接,形成传输数据通道,似于拨打电话
  • 传输前,采用“三次握手”方式,属于点对点通信,是面向连接的,效率低
  • 仅支持单播传输,每条TCP传输连接只能有两个端点(客户端、服务端)。 两个端点的数据传输,采用的是“字节流”来传输,属于可靠的数据传输
  • 传输完毕,需释放已建立的连接,开销大,速度慢,适用于文件传输、邮件等。
UDP协议
  • 采用数据报(数据、源、目的)的方式来传输,无需建立连接,类似于发短信
  • 每个数据报的大小限制在64K内,超出64k可以分为多个数据报来发送
  • 发送不管对方是否准备好,接收方即使收到也不确认,因此属于不可靠的
  • 可以广播发送,也就是属于一对一、一对多和多对一连接的通信协议
  • 发送数据结束时无需释放资源,开销小,速度快,适用于视频会议、直播等
TCP协议和UDP协议的区别
描述TCPUDP
是否连接面向连接面向非连接
传输可靠性可靠不可靠
连接对象个数一对一一对一、一对多、多对一
传输方式面向字节流面向报文
传输速度
应用场景适用于实时应用(视频会议、直播等)适用于可靠传输(文件传输、邮件等)

TCP三次握手和四次挥手

  • 客户端与服务端在使用TCP传输协议时要先建立一个“通道”,在传输完毕之后又要关闭这“通道”,前者可以被形象地成为“三次握手”,而后者则可以被称为“四次挥手”
三次握手
  • 通道的建立之三次握手

    1. 在建立通道时,客户端首先要向服务端发送一个SYN同步信号
    2. 服务端在接收到这个信号之后会向客户端发出SYN同步信号和ACK确认信号
    3. 当服务端的ACK和SYN到达客户端后,客户端与服务端之间的这个“通道”就被建立
  • 具体如下图

    网络编程-TCP和UDP协议-2

四次挥手
  • 通道的关闭之四次挥手

    1. 在数据传输完毕之后,客户端会向服务端发出一个FIN终止信号
    2. 服务端在收到这个信号之后会向客户端发出一个ACK确认信号
    3. 如果服务端此后也没有数据发给客户端时服务端会向客户端发送一个FIN终止信号
    4. 客户端在收到这个信号之后会回复一个确认信号,在服务端接收到这个信号之后,服务端与客户端的通道也就关闭了
  • 具备如下图

    网络编程-TCP和UDP协议-3

基于TCP协议的编程

概述

  • 套接字是一种进程间的数据交换机制,利用套接字(Socket)开发网络应用程序早已被广泛的采用,以至于成为事实上的标准

  • 在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client),而在第一次通讯中等待连接的程序被称作服务端(Server) 。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别

  • 套接字与主机地址和端口号相关联,主机地址就是客户端或服务器程序所在的主机的IP地址,端口地址是指客户端或服务器程序使用的主机的通信端口。在客户端和服务器中,分别创建独立的Socket,并通过Socket的属性,将两个Socket进行连接,这样客户端和服务器通过套接字所建立连接并使用IO流进行通信

  • 具体如下图

    网络编程-TCP和UDP协议-4

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语言的基于套接字编程分为服务端编程和客户端编程,其通信模型如图所示

    网络编程-基于TCP协议的编程-1

服务器端实现步骤
  • 步骤如下

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

    • 应用实例1,服务器端实现

      package com.bjpowernode.demo;

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

      /**
      * Socket服务器端实现
      */
      public class SocketServerDemo {
      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();
      }
      }
      }
      }
      }
客户端实现步骤
  • 步骤如下

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

    • 应用实例,客户端实现

      package com.bjpoernode.demo;

      import java.io.IOException;
      import java.io.OutputStreamWriter;
      import java.io.Writer;
      import java.net.InetAddress;
      import java.net.Socket;

      /**
      * Socket客户端实现
      */
      public class SocketClientDemo {
      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实现,运行了解效果即可
服务器端实现
package com.bjpowernode.demo;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
* Socket服务器端
* 【重点】1、每个客户端socket请求,开启一个线程;2、每个线程都使用一个死循环,等待客户端的数据请求
*/
public class TalkServer {
public static void main(String[] args) throws IOException {
//构建Socket服务
ServerSocket serverSocket = new ServerSocket(8000); // 监听指定端口
System.out.println("本地地址:"+serverSocket.getLocalSocketAddress());
System.out.println("【服务器已启动】");
//构建一个死循环,一直等待客户端Socket连接
for (;;) {
//阻塞接收一个Socket请求
Socket socket = serverSocket.accept();
System.out.println(String.format("【客户端连接已创建(客户端地址:%s)】",socket.getRemoteSocketAddress()));
//每一个socket请求启动一个线程处理
Thread t = new Handler(socket);
t.start();
}
}
}

/**
* socket请求处理线程,接收当前socket对象,并持续接收socket客户端传入数据,直到输入end为止
*/
class Handler extends Thread {
Socket socket;

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

@Override
public void run() {
//获取Socket输入流
try (InputStream input = this.socket.getInputStream()) {
//获取Socket输出流
try (OutputStream output = this.socket.getOutputStream()) {
//处理当前Socket请求数据
handle(input, output);
}
} catch (Exception e) {
try {
this.socket.close();
} catch (IOException ioe) {
}
System.out.println("【客户端连接已断开】");
}
}

/**
* 处理请求逻辑,通过一个列循环,一直等待用户输入信息
* @param input
* @param output
* @throws IOException
*/
private void handle(InputStream input, OutputStream output) throws IOException {
//使用Socket输入流构造带缓冲的输入字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
//使用Socket输出流构造带缓冲的输出字符流
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
//输出连接成功
writer.write("连接成功!\n");
writer.flush();
//构建一个死循环,一直等待当前Socket客户端的信息
for (;;) {
//读取一行消息
String message = reader.readLine();
//输出当前Socket远端请求地址、线程名称、客户端发送的信息
System.out.println(String.format("【收到消息(客户端地址:%s,线程名:%s)】%s",socket.getRemoteSocketAddress(),Thread.currentThread().getName(),message));
//如果该行信息不是end,把从Socket客户端接收的信息包装返回;如果是end,结束死循环,Socket连接断开
if (message.equalsIgnoreCase("end") == false) {
writer.write("你发送的消息已经收到,你发送的消息是[" + message + "]\n");
writer.flush();
}else{
writer.write(String.format("[%s]你发送的断开连接请求,再见!\n",this.socket.getRemoteSocketAddress()));
writer.flush();
System.out.println(String.format("【客户端连接已断开(客户端地址:%s,线程名:%s)】",socket.getRemoteSocketAddress(),Thread.currentThread().getName()));
break;
}
}
}
}
客户端实现
package com.bjpowernode.demo;

import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;

/**
* Socket客户端
* 【重点】1、客户端使用使用一个死循环,持续等待用户的输入,直到输入end为止
*/
public class TalkClient {
public static void main(String[] args) throws IOException {
//构建Socket客户端,指定Socket服务器
Socket socket = new Socket("localhost", 8000);
System.out.println("本地地址:"+socket.getLocalSocketAddress());
System.out.println("服务器地址:"+ socket.getRemoteSocketAddress());

//获取Socket输入流
try (InputStream input = socket.getInputStream()) {
//获取Socket输出流
try (OutputStream output = socket.getOutputStream()) {
//处理当前Socket请求
handle(input, output);
}
}
socket.close();
System.out.println("【连接断开】");
}

/**
* 客户端面处理方法,通过死循环持续接收用户输入,直到输入end
* @param input
* @param output
* @throws IOException
*/
private static void handle(InputStream input, OutputStream output) throws IOException {
//使用Socket输入流构造带缓冲的输入字符流
BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
//使用Socket输出流构造带缓冲的输出字符流
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
//构造一个标准输入流
Scanner scanner = new Scanner(System.in);
System.out.println("【来自服务器端的消息】 " + reader.readLine());
//构建一个死循环,持续接收控制台的输入,并发送给Socket服务器
for (;;) {
//输出提示信息
System.out.print("请输入你要发送给服务器端的消息:");
//接收控制台输入
String message = scanner.nextLine();

//将控制台输入信息写入输出流
writer.write(message);
writer.newLine();
writer.flush();

//读取输入流,获取Socket服务器返回信息
String response = reader.readLine();
System.out.println("【来自服务器端的消息】 " + response);

//如果当前用户输入的为end,结束当前死循环,Socket请求结束
if (message.equals("end")) {
break;
}
}
}
}

【练习】

  1. 练习应用实例内容,完成代码编写

基于UDP协议的编程【扩展】

UDP协议编程的概述

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

DatagramSocket类的概述

  • DatagramSocket类作为基于UDP协议的Socket,使用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()获得发送或接收数据报中的长度

基于UDP编程的实现

接收端实现步骤
  • 实现步骤如下

    1. 创建DatagramSocket对象(接收端),并指定端口号
    2. 创建DatagramPacket对象(数据报)
    3. 调用receive()方法,用于接收数据报;
    4. 调用close()方法关闭资源
  • 应用实例

    • 应用实例1,接收端实现

      package com.bjpowernode.demo;

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

      /**
      * DatagramSocket接收端实现
      */
      public class DatagramSocketServerDemo {
      public static void main(String[] args) {
      DatagramSocket datagramSocket = null;
      try {
      // 实例化DatagramSocket对象(接收端),并指定端口号
      datagramSocket = new DatagramSocket(8888);
      // 创建数据报对象,用于接收发送端传递的数据
      byte[] bytes = new byte[1024 * 64];
      DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length);
      // 接收发送端的传递过来的数据
      System.out.println("等待接收发送端数据...");
      datagramSocket.receive(datagramPacket);
      // 获得数据报对象中存储的数据
      byte[] data = datagramPacket.getData();
      int length = datagramPacket.getLength();
      String str = new String(data, 0, length);
      System.out.println(str);
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      // 关闭资源
      datagramSocket.close();
      }
      }
      }
发送端实现
  • 实现步骤如下

    1. 创建DatagramSocket对象(发送端)
    2. 创建DatagramPacket对象(数据报),并指定接收端IP地址和端口;
    3. 调用send()方法,用于发送数据报
    4. 调用close()方法关闭资源
  • 应用实例

    • 应用实现1,发送端实现

      package com.bjpowernode.demo;

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

      /**
      * DatagramSocket发送端实现
      */
      public class DatagramSocketClientDemo {
      public static void main(String[] args) {
      DatagramSocket datagramSocket = null;
      try {
      // 创建数据报套接字(发送端)
      datagramSocket = new DatagramSocket();
      // 创建数据报对象用于封装传送的数据,并设置连接接收端的IP地址和端口
      byte[] bytes = "为人民服务!".getBytes();
      InetAddress inetAddress = InetAddress.getByName("127.0.0.1");
      DatagramPacket datagramPacket = new DatagramPacket(bytes, 0, bytes.length, inetAddress, 8888);
      // 发送数据
      datagramSocket.send(datagramPacket);
      } catch (IOException e) {
      e.printStackTrace();
      } finally {
      // 关闭资源
      datagramSocket.close();
      }
      }
      }

【练习】

  1. 练习应用实例内容,完成代码编写

RMI简介【扩展】

概述

  • RMI(Remote Method Invocation),是一种基于契约的通信机制,可以理解为是RPC协议(Remote Procedure Call Protocol) 的一种Java实现
  • 面向对象,而且相比Socket更加简洁、方便、易用
  • 通过RMI,一个JVM进程中的程序可以调用网络中另外一个JVM进程中的具有相同签名的方法,一般是让两个JVM中的调用和被调用方实现同一个接口
  • 相比RESTful API,RPC有更严格的接口约束条件,使用二进制传输也有更高的效率
  • 常用的RPC框架有:Dubbo、gRPC
  • Java提供了java.rmi.Remote实现RMI

应用实例

  • 应用实例结构图

    网络编程简介-rmi

应用实例结构图
  • 主要业务逻辑

    • Remote接口,提供了服务端和客户端通信的契约
      • 服务类,如IService
      • 实体类,如CommonEntity
    • 服务端,提供了服务契约的实现,依赖Remote接口项目,如针对IService的实现ServiceImpl
    • 客户端,根据服务契约的规范,像项目自己服务一样,使用服务端提供的服务,依赖Remote接口项目
  • 实现步骤及代码

    • 第一步:定义公共接口和公共实体,代码见附件中的rmi-remote.zip
      • 定义公共接口,需要实现Remote接口,公共接口的方法都抛出RemoteException
      • 定义公共实体,作为公共接口的输入/输出参数,需要实体要实现Serializable,用于网络传输时序列化/反序列化
      • 构建成jar包供服务端和客户端使用
        • 打开菜单File->Project Structure配置
        • 选择Project Settgings中的Artifacts,并点击+号
        • 选择JAR->From modules with dependencies…,点击ok
        • 然后,打开菜单Build->Build artifacts…,选择项目名:jar->build,将会在目录out\artifacts下生成一个jar包
    • 第二步:定义服务端服务;代码见附件中的rmi-server.zip
      • 首先,引入第一步定义的jar包
      • 然后,实现上一步定义所有服务接口
      • 再有,使用LocateRegistry. createRegistry指定端口提供RMI服务
      • 最后,使用创建的registry,并用bind方法将服务绑定到服务名
    • 第三步:定义客户端调用,代码见附件中的rmi-client.zip
      • 首先,引入第一步定义的jar包
      • 使用java.rmi.Naming.lookup通过rmi的URL查找服务并使用

    【练习】

    1. 练习应用实例内容,完成代码编写
    2. 编写程序,参考实例内容,完成一个学生信息的基于RMI的增、删、改、查服务
      1. Remote接口项目,提供实体和接口契约,
      2. 服务端项目,实现Remote接口项目,并提供实际服务,学生信息存储于List(熟悉JDBC的同学,可使用JDBC将数据落地到数据库)
      3. 客户端项目,使用服务端项目,实现的数据增、删、改、查