如今,计算机已经成为人们学习、工作、生活必不可少的工具。人们利用计算机可以和亲朋好友在网上聊天,玩网游或发邮件等,这些功能的实现都离不开计算机网络。计算机网络实现了不同计算机之间的通信,而这些必须依靠人们编写网络程序来实现。在Java中提供相应的类包让大家编写网络程序,即下文说要提到的内容。

基础概念

在学习如何编程之前,首先要了解关于网络通信的一些概念。本文仅介绍基础的概念,如果想了解相关的知识,可以翻阅相关的书籍。

计算机网络

计算机网络是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统、网络管理软件及网络通信协议的管理和协调下,实现资源共享和信息传递的计算机系统。

简要的说就是以下内容:

  1. 计算机网络的作用是资源共享和信息传递。
  2. 计算机网络的组成包括:
    • 计算机硬件:计算机(大中小型服务器、台式机、笔记本等)、外部设备(路由器、交换机等)、通信线路(双绞线、光纤等)。
    • 计算机软件:网络操作系统(Windows Server/Adcance Server、Unix、Linux等)、网络管理软件(WorkWin、SugarNMS等)、网络通信协议(TCP/IP等)。
  3. 计算机网络中的多台计算机是具有独立功能的,而不是脱离网络就无法存在的。

网络通信协议

  • 网络通信协议

国际标准组织定义了网络通信协议的基本框架,被称为开放系统互联模型,即OSI模型。OSI模型将通信标准按层次进行划分,每一个层次解决一个类型的问题,这样就是的标准的制定没那么复杂。OSI模型制定的七层标准模型,分别是应用层、表示层、会话层、传输层、网络层、数据链路层和物理层。

OSI的七层协议模型如下图所示:

OSI的七层协议模型

虽然国际化标准组织制定了这样一个网络通信协议的模型,但是实际上互联网通信使用最多的还是TCP/IP网络通信协议。

TCP/IP是一个协议族,按照层次划分为四层,分别是应用层、传输层、互连网络层和网络接口层(物理+数据链路层)。

OSI网络通信协议模型仅是一个参考模型,而TCP/IP协议是事实上的标准。TCP/IP协议参考了OSI模型但是并没有严格按照OSI规定的七层标准去划分,而只划分了四层,这样会更简单。以免在划分太多层次时,人们很难区分某个协议是属于哪个层次的。TCP/IP中有两个重要的协议,传输层的TCP协议和互连网络层的IP协议,因此就拿这两个协议来命名整个协议族,TCP/IP协议就是指整个协议族。

  • 网络协议的分层

由于网络节点之间的联系很复杂,因此协议把复杂的内容分解成简单的内容,再将它们复合起来。最常用的复合方式是层次方式,即同层间可以通信,上一层可以调用下一层,而与再下一层不***。

用户应用程序为最高层,物理通信线路为最低层,其间的协议处理分为若干层并规定每层处理的任务,也规定每层的接口标准。

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

数据封装与解封

由于用户传输的数据一般都比较大,甚至以兆字节计算,一次发送出去十分困难,因此就需要把数据分成很多片段,再按照一定的次序发送出去。这个过程就需要对数据进行封装。

数据封装(Data Encapsulation)是指将协议数据单元(PDU)封装在一组协议头和协议尾中的过程。在OSI七层参考模型中,每层主要负责与其他机器上的对等层进行通信。该过程是在协议数据单元(PDU)中实现的,其中每层的PDU一般由本层的协议头、协议尾和数据封装构成。

  1. 数据发送处理过程
    • 应用层将数据转交给传输层,传输层添加上TCP的控制信息(称为TCP头部),这个数据单元称为段(Segment),加入控制信息的过程称为封装。然后,将段交给网络层。
    • 网络层接收到段,再添加上IP头部,这个数据单元称为包(Packet)。然后,将包交给数据链路层。
    • 数据链路层接收到包,在添加MAC头部和尾部,这个数据单元称为帧(Frame)。然后将帧交给物理层。
    • 物理层将接收到的数据转化为比特流,然后在网线中传输。
  2. 数据接收处理过程
    • 物理层接收到比特流,经过处理后将数据交给数据链路层。
    • 数据链路层将接收到的数据转化为数据帧,再去除MAC头部和尾部,这个去除控制信息的过程称为解封,然后将包交给网络层。
    • 网络层接收到包,再去除IP头部,然后将段交给传输层。
    • 传输层接收到段,再去除TCP头部,然后将数据交给应用层。

综上所述,我们可以总结如下:

  • 发送方的数据处理方式是从高层到底层,逐层进行数据封装。
  • 接收方的数据处理方式是从底层到高层,逐层进行数据解封。

接收方的每一层只把对该层有意义的数据拿走,或者说每一层只能处理与发送方同等层的数据,然后把其余的部分传递给上一层,这就是对等层通信的概念。

IP地址与端口

  • IP地址

IP地址用来标识网络中的一个通信实体的地址。通信实体可以是计算机、路由器等。例如,互联网的每个服务器都要有自己的IP地址,而局域网的每台计算机要进行通信也要配置IP地址。路由器是连接多个网络的网络设备。

目前主流IP地址使用的是IPv4协议,但是随着网络规模的不断扩大,采用IPv4协议的可用地址数量面临着枯竭的危险,所以推出IPv6协议。

IPv4协议采用32位地址,并以8位为一个单位,分成四部分,以点分十进制表示。如192.168.0.1。8位二进制的计数范围是00000000 ~ 11111111,对应十进制的0 ~ 255。

IPv6协议为128位地址(16字节),写成8个16位的无符号整数。每个整数用4个十六进制位表示,每个数之间用冒号(:)隔开,例如:3ffe:3201:1280:c8ff:fe4d:db39:1984。

在IP地址中有一些特殊的地址:

127.0.0.1为本机地址。
192.168.0.0~192.168.255.255为私有地址,属于非法注册地址,专门为组织机构内部使用。
  • 端口

IP地址用来标识一台计算机,但是一台计算机上可能提供多种网络应用程序,区分这些应用程序就需要使用到了端口。

端口是虚拟的概念,并不是在主机上真的有若干个端口。通过端口,可以在一台主机上运行多个网络程序。端口用一个16位的二进制整数表示,对用十进制的范围就是0~65535。

IP地址就像是每个人的门牌号,而端口就是房间号。必须同时指定IP地址和端口号才能够正确发送接收数据。

URL

在因特网上,每一个信息资源都有统一且唯一的地址,该地址就叫做URL(Uniform Resource Locator),它是因特网的统一资源定位符。URL由四部分所组成:协议、存放资源的主机域名、资源文件名和端口号。如果未指定端口号,则使用协议默认的端口。例如HTTP协议的默认端口号为80。在浏览器中访问网页时,地址栏显示的地址就是URL。

在Java中,java.util包中提供了URL类,该类封装了大量涉及从远程站点获取信息的复杂细节。

Socket

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

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

TCP协议和UDP协议

TCP协议和UDP协议的联系与区别

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

TCP与UDP的主要区别:

  • TCP是面向连接的,传输数据安全、稳定,效率相对较低。TCP就类似于打电话,使用这种方式进行网络通信时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重新发送该数据。
  • UDP是面向无连接的,传输数据不安全,但效率较高。UDP则类似于发送短信,使用这种方式进行网络通信时,不需要建立专门的虚拟连接,传输也是不是很可靠,如果发送失败则客户端无法获得数据。

TCP协议

TCP协议是面向连接的,所谓面向连接,就是当计算机双方通信时必须经过先建立连接,然后传送数据,最后拆除连接三个过程。

TCP在建立连接时分为以下三步:

  1. 请求端/客户端发送一个含SYN即同步(Synchronize)标志的TCP报文,SYN同步报文会指明客户端使用的端口以及TCP连接的初始序号。
  2. 服务器在收到客户端的SYN报文后,将返回一个SYN+ACK(确认Acknowledgement)报文,表示客户端的请求被接收。同时TCP序号被加1。
  3. 客户端返回一个确认报文ACK给服务器端,同样TCP序号被加1,至此一个TCP连接完成。然后才开始通信的第二部,数据处理。

以上就是场所的TCP的三次握手(Three-way Handshake)。

UDP协议

基于TCP协议可以建立稳定连接的点对点通信。这种通信方式实时、快速、安全性高,但是很占用系统的资源。

在网络传输上,还有另一种基于UDP协议的通信方式,称为数据报通信方式。在这种方式中,每个数据发送单元被统一封装成数据报包的方式,发送方将数据报包发送到网络,数据报包在网络中去寻找它的目的地。

Java网络编程中的常用类

在Java中,为了可移植性,不允许直接调用操作系统,而是由java.net包来提供网络功能。Java虚拟机负责提供与操作系统的实际连接,下文将介绍结构java.net包中的常用类。

InetAddress

  1. 作用:InetAddress用于封装计算机的IP地址和DNS(没有端口信息)。
  2. 特点:InetAddress类没有构造器。如果要得到对象,只能通过静态方法getLocalHost()、getByName()、getAllByName()、getAddress()和getHostName()实现。

下面是这些方法的使用例子:

import java.net.InetAddress;
import java.net.UnknownHostException;
/**
 * IP:定位一个节点:计算机、路由、通讯设备等
 * InetAddress两个静态方法:getLocalHost():本机    getByName():根据域名解析(DNS)|根据IP地址解析IP地址
 * 两个成员方法: getHostAddress():返回地址|getHostName():返回计算机名或域名
 * @author WHZ
 *
 */
public class IPTest {
    public static void main(String[] args) throws UnknownHostException {
        //使用getLocalHost方法创建InetAddress对象
        InetAddress address=InetAddress.getLocalHost();
        System.out.println(address.getHostAddress());  //返回address的IP地址:192.168.31.76
        System.out.println(address.getHostName());  //返回主机名:WHZ-PC
        //根据域名得到InetAddress对象
        address=InetAddress.getByName("baidu.com");
        System.out.println(address.getHostAddress());  //返回address的IP地址:220.181.38.148
        System.out.println(address.getHostName());   //输出baidu.com
        //根据IP得到InetAddress对象
        address=InetAddress.getByName("49.232.138.233");
        System.out.println(address.getHostAddress());  //返回address的IP地址:49.232.138.233
        System.out.println(address.getHostName());  //通过IP地址解析域名
    }
}

InetSocketAddress

InetSocketAddress用于包含IP地址和端口信息,常用语Socket通信。该类实现IP套接字地址(IP地址+端口号),不依赖任何协议。

import java.net.InetSocketAddress;
/**
 * 端口:1.区分软件 2.两个字节:0-65535 3.TCP和UDP协议,同一个协议端口不能冲突 4.定义端口越大越好
 * InetSocketAddress:1.构造器new InetSocketAddress(地址|域名, 端口);
 * 2.方法:getAddress()|getHostName()|getPort()
 * @author WHZ
 *
 */
public class PortTest {
    public static void main(String[] args) {
        //包含端口
        InetSocketAddress socketAddress1=new InetSocketAddress("127.0.0.1", 8080);
        InetSocketAddress socketAddress2=new InetSocketAddress("localhost", 9000);
        System.out.println(socketAddress1.getHostName());  //获得主机名
        System.out.println(socketAddress2.getAddress());  //获得地址
        System.out.println(socketAddress1.getPort());  //获得端口
        System.out.println(socketAddress1.getHostString());  //获得主机名字符串
    }
}

URL类

IP地址唯一标识了Internet上的计算机,而URL则标识了这些计算机上的资源。URL类代表一个统一资源定位符,它是指向互联网资源的指针。资源可以是简单的文件或者目录,也可以是对更为复杂对象的引用,例如对数据库或搜索引擎进行查询。

为了方便程序员编程,JDK提供了URL类,该类的全名是java.net.URL。有了这样一个类,就可以使用它的各种方法来对URL对象进行分割、合并等处理。

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

/**
 * URL:统一资源定位器,互联网三大基石之一(URL,html,http),区分资源
 * 1.协议    2.域名|IP|计算机    3.端口    4.请求资源
 * http://www.baidu.com:80/index.html?uname=whz&age=21#a
 * @author WHZ
 *
 */
public class URLTest {
    public static void main(String[] args) throws MalformedURLException {
        URL url=new URL("http://www.baidu.com:80/index.html?uname=whz&age=21#a");
        //获取四个值
        System.out.println("协议:"+url.getProtocol());
        System.out.println("域名|IP|计算机:"+url.getHost());
        System.out.println("端口:"+url.getPort());
        System.out.println("请求资源1:"+url.getFile());
        System.out.println("请求资源2:"+url.getPath());
        System.out.println("参数:"+url.getQuery());
        System.out.println("锚点:"+url.getRef());
    }
}

简单的爬虫实现

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;

/**
 * 网络爬虫的原理
 * @author WHZ
 *
 */
public class SpiderTest01 {
    public static void main(String[] args) throws Exception {
        //获取资源
        URL url=new URL("https://www.jd.com");
        //下载资源
        File file=new File("jd.txt");
        InputStream is=url.openStream();
        BufferedReader br=new BufferedReader(new InputStreamReader(is,"UTF-8"));
        BufferedWriter bw=new BufferedWriter(new FileWriter(file));
        String msg=null;
        while (null!=(msg=br.readLine())) {
            System.out.println(msg);
            bw.append(msg);
            bw.newLine();
        }
        bw.close();
        br.close();
        //分析
        //处理。。。
    }
}

TCP通信的实现

上文提到TCP协议是面向连接的,在通信时客户端与服务器端必须建立连接。在网络通信中,第一次主动发起通信的程序被称为客户端(Client)程序,简称客户端;而在第一次通信中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通信建立,则客户端和服务器端完全一样,没有本质的区别。

“请求-响应”模式

在“请求-响应”模式中,Socket类用于发送TCP消息;ServerSocket类用于创建服务器。

套接字Socket是一种进程间的数据交换机制。这些进程既可以在同一机器上,也可以在通过网络连接的不同机器上。换句话说,套接字起到了通信的作用。单个套接字是一个端点,而一对套接字则构成一个双向通信信道,使非关联程序可以在本地或通过网络进行数据交换。一旦建立套接字连接,数据即可在相同或不同的系统中双向或单向发送,直到其中一个端点关闭连接。套接字与主机地址和端口地址相关联。主机地址就是客户端或服务器程序所在主机的IP地址。端口地址是指客户端或服务端使用的主机的通信端口。

在客户端和服务端中,分别创建独立的Socket,并通过Socket的属性将两个Socket进行连接,这样,客户端和服务端通过套接字所建立的连接即可使用输入/输出流进行通信。

TCP/IP套接字是最可靠的双向流协议,使用TCP/IP可以发送任意数量的数据。

实际上,套接字只是计算机上已编号的端口。如果发送方和接收方计算机确定好端口,它们之间就可以进行通信了。

TCP/IP通信连接的简单过程

TCP/IP通信连接过程:位于A计算机上的TCP/IP软件向B计算机发送包含端口号和消息;B计算机的TCP/IP软件接收该消息并进行检查,查看是否有它知道的程序正在该端口上接收消息。如果有,它将该消息交给这个程序。要是程序有效运行,就必须有一个客户端和一个服务器。

通过Socket的编程顺序

  1. 创建服务器ServerSocket。在创建时,定义ServerSocket的监听端口(在这个端口接收客户端发来的消息)。
  2. ServerSocket调用accept()方法,使之处于阻塞状态。
  3. 创建客户端Socket,并设置服务器的IP地址及端口。
  4. 客户端发出连接请求,建立连接。
  5. 分别取得服务器和客户端Socket的InputStream和OutputStream。
  6. 利用Socket和ServerSocket进行数据传输。
  7. 关闭流及Socket。

以下是TCP通信的实际使用例子,并包括一个聊天室的实现,可以结合代码理解使用(为了简化代码,代码中的异常都使用throws抛出了,实际开发中请遵循try_catch_finally的方法抛出异常)。

TCP单向通信例子

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

/**
 * 熟悉流程:创建服务器
 * 1.指定端口 使用ServerSocket创建服务器
 * 2.阻塞式等待连接accept()
 * 3.操作:输入流输出流操作
 * 4.释放资源
 * @author WHZ
 *
 */
public class Server {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Server----------");
        //1.指定端口 使用ServerSocket创建服务器
        ServerSocket server=new ServerSocket(8888);
        //2.阻塞式等待连接accept()
        Socket client=server.accept();  //阻塞式
        System.out.println("一个客户端建立了连接。");
        //3.操作:输入流输出流操作
        DataInputStream dis=new DataInputStream(client.getInputStream());
        String str=dis.readUTF();
        System.out.println(str);
        //4.释放资源
        dis.close();
        client.close();

        server.close();
    }
}
import java.io.DataOutputStream;
import java.net.Socket;

/**
 * 熟悉流程:创建客户端
 * 1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
 * 2.操作:输入流输出流操作
 * 3.释放资源
 * @author WHZ
 *
 */
public class Client {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Client----------");
        //1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
        Socket client=new Socket("localhost", 8888);
        //2.操作:输入流输出流操作
        DataOutputStream dos=new DataOutputStream(client.getOutputStream());
        dos.writeUTF("Hello World");
        dos.flush();
        //3.释放资源
        dos.close();
        client.close();
    }
}

单向通信实现(模拟登陆)

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

/**
 * 模拟登陆 单向:创建服务器
 * 1.指定端口 使用ServerSocket创建服务器
 * 2.阻塞式等待连接accept()
 * 3.操作:输入流输出流操作
 * 4.释放资源
 * @author WHZ
 *
 */
public class LoginServer {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Server----------");
        //1.指定端口 使用ServerSocket创建服务器
        ServerSocket server=new ServerSocket(8888);
        //2.阻塞式等待连接accept()
        Socket client=server.accept();
        //3.操作:输入流输出流操作
        DataInputStream dis=new DataInputStream(client.getInputStream());
        String data=dis.readUTF();
        //分析
        String[] datas=data.split("&");
        for (String info:datas) {
            String[] userInfo=info.split("=");
            if (userInfo[0].equals("uname")) {
                System.out.println("你的用户名为:"+userInfo[1]);
            } else if (userInfo[0].equals("upwd")) {
                System.out.println("你的密码为:"+userInfo[1]);
            }
        }
        //4.释放资源
        dis.close();
        client.close();

        server.close();
    }
}
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模拟登陆 单向:创建客户端
 * 1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
 * 2.操作:输入流输出流操作
 * 3.释放资源
 * @author WHZ
 *
 */
public class LoginClient {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Client----------");
        //1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
        Socket client=new Socket("localhost", 8888);
        //2.操作:输入流输出流操作
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        System.out.print("请输入用户名:");
        String uname=br.readLine();
        System.out.print("请输入密码:");
        String upwd=br.readLine();
        DataOutputStream dos=new DataOutputStream(client.getOutputStream());
        dos.writeUTF("uname="+uname+"&upwd="+upwd);
        dos.flush();
        //3.释放资源
        dos.close();
        br.close();
        client.close();

    }
}

双向通信实现(模拟登陆服务器返回消息)

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

/**
 * 模拟登陆 双向:创建服务器
 * 1.指定端口 使用ServerSocket创建服务器
 * 2.阻塞式等待连接accept()
 * 3.操作:输入流输出流操作
 * 4.释放资源
 * @author WHZ
 *
 */
public class LoginTwoWayServer {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Server----------");
        //1.指定端口 使用ServerSocket创建服务器
        ServerSocket server=new ServerSocket(8888);
        //2.阻塞式等待连接accept()
        Socket client=server.accept();
        //3.操作:输入流输出流操作
        DataInputStream dis=new DataInputStream(client.getInputStream());
        String data=dis.readUTF();
        String uname="";
        String upwd="";
        String[] datas=data.split("&");
        for(String info:datas) {
            String[] userInfo=info.split("=");
            if (userInfo[0].equals("uname")) {
                uname=userInfo[1];
                System.out.println("你的用户名为:"+userInfo[1]);
            }else if (userInfo[0].equals("upwd")) {
                upwd=userInfo[1];
                System.out.println("你的密码为:"+userInfo[1]);
            }
        }
        DataOutputStream dos=new DataOutputStream(client.getOutputStream());
        if (uname.equals("abc")&&upwd.equals("123456")) {
            dos.writeUTF("登陆成功,欢迎回来。");
        }else {
            dos.writeUTF("登陆失败,请检查账号密码。");
        }
        //4.释放资源
        dos.close();
        dis.close();
        client.close();

        server.close();
    }
}
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模拟登陆 双向:创建客户端
 * 1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
 * 2.操作:输入流输出流操作
 * 3.释放资源
 * @author WHZ
 *
 */
public class LoginTwoWayClient {
    public static void main(String[] args)throws Exception {
        System.out.println("----------Client----------");
        //1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
        Socket client=new Socket("localhost", 8888);
        //2.操作:输入流输出流操作
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        System.out.print("请输入用户名:");
        String uname=br.readLine();
        System.out.print("请输入密码:");
        String upwd=br.readLine();
        DataOutputStream dos=new DataOutputStream(client.getOutputStream());
        dos.writeUTF("uname="+uname+"&upwd="+upwd);
        dos.flush();
        DataInputStream dis=new DataInputStream(client.getInputStream());
        String msg=dis.readUTF();
        System.out.println(msg);
        //3.释放资源
        dis.close();
        dos.close();
        br.close();
        client.close();
    }
}

多账号登陆的实现

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 模拟登陆 多个客户端请求:创建服务器
 * 1.指定端口 使用ServerSocket创建服务器
 * 2.阻塞式等待连接accept()
 * 3.操作:输入流输出流操作
 * 4.释放资源
 * @author WHZ
 *
 */
public class LoginMultiServer {
    public static void main(String[] args) throws Exception{
        System.out.println("----------Server----------");
        //1.指定端口 使用ServerSocket创建服务器
        ServerSocket server=new ServerSocket(6666);
        boolean isRunning=true;
        while (isRunning) {
            //2.阻塞式等待连接accept()
            Socket client=server.accept();
            //3.操作:输入流输出流操作
            new Thread(new Channel(client)).start();
        }
        //4.释放资源
        server.close();
    }

    static class Channel implements Runnable{
        private Socket client;
        private DataInputStream dis=null;  //输入流
        private DataOutputStream dos=null;  //输出流
        public Channel(Socket client) {
            this.client=client;
            try {
                dis=new DataInputStream(client.getInputStream());  //输入
                dos=new DataOutputStream(client.getOutputStream());  //输出
            } catch (IOException e) {
                e.printStackTrace();
                close();
            }
        }
        //接收数据
        private String receive() {
            String data="";
            try {
                data=dis.readUTF();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return data;
        }
        //发送数据
        private void send(String msg) {
            try {
                dos.writeUTF(msg);
                dos.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void run() {
            System.out.println("建立了一个客户端连接");
            String uname="";
            String upwd="";
            String[] datas=receive().split("&");
            for (String info:datas) {
                String[] userInfo=info.split("=");
                if (userInfo[0].equals("uname")) {
                    uname=userInfo[1];
                    System.out.println("你的用户名是:"+uname);
                }else if (userInfo[0].equals("upwd")) {
                    upwd=userInfo[1];
                    System.out.println("你的密码是:"+upwd);
                }
            }
            if (uname.equals("abc") && upwd.equals("123456")) {
                System.out.println("账号:"+uname+",登陆成功,欢迎回来。");
                send("账号:"+uname+",登陆成功,欢迎回来。");
            }else {
                System.out.println("账号:"+uname+",登陆失败,请检测账号密码。");
                send("账号:"+uname+",登陆失败,请检测账号密码。");
            }
            close();
        }
        //释放资源
        private void close() {
            try {
                if (null!=dos) {
                dos.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (null!=dis) {
                    dis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (null!=client) {
                    client.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
import java.io.BufferedReader;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 模拟登陆 多个客户端请求:创建客户端
 * 1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
 * 2.操作:输入流输出流操作
 * 3.释放资源
 * @author WHZ
 *
 */
public class LoginMultiClient {
    public static void main(String[] args)throws Exception {
        System.out.println("----------Client----------");
        //1.建立连接 使用Socket创建客户端,需要指定服务器的地址和端口
        Socket client=new Socket("localhost",6666);
        //2.操作:输入流输出流操作
        new Send(client).send();
        new Receive(client).receive();
        //3.释放资源
        client.close();
    }
    //发送
    static class Send{
        private Socket client;
        private DataOutputStream dos;
        private BufferedReader br;
        private String msg;
        public Send(Socket client) {
            br=new BufferedReader(new InputStreamReader(System.in));
            this.msg=init();
            this.client=client;
            try {
                dos=new DataOutputStream(client.getOutputStream());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //初始化
        private String init() {
            try {
                System.out.print("请输入用户名:");
                String uname=br.readLine();
                System.out.print("请输入密码:");
                String upwd=br.readLine();
                return "uname="+uname+"&upwd="+upwd;
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            return null;
        }

        public void send() {
            try {
                dos.writeUTF(msg);
                dos.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }
    //接收
    static class Receive{
        private Socket client;
        private DataInputStream dis=null;
        public Receive(Socket client) {
            this.client=client;
            try {
                dis=new DataInputStream(client.getInputStream());
            } catch (IOException e) {

                e.printStackTrace();
            }
        }

        public void receive() {
            String msg=null;
            try {
                msg=dis.readUTF();
                System.out.println(msg);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

聊天室的设计与实现

此部分代码仅供参考,对于新手,相关的聊天室项目,可以去网上寻找相关的博客或者更详细的讲解视频来理解学习。

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * 在线聊天室:服务器
 * 目标:私聊
 * @author WHZ
 *
 */
public class Chatroom {
    private static CopyOnWriteArrayList<Channel> all=new CopyOnWriteArrayList<Channel>();
    public static void main(String[] args) throws Exception{
        System.out.println("聊天室启动中...");
        ServerSocket server=new ServerSocket(8888);  //创建服务器
        boolean operatingCondition=true;
        while (operatingCondition) {
            Socket client=server.accept();  //接收客户端访问(阻塞式接收)
            System.out.println("一个客户端建立了连接");
            Channel channel=new Channel(client);
            all.add(channel);  //管理所有的成员
            new Thread(channel).start();
        }
        server.close();
    }
    //一个客户端代表一个Channel
    static class Channel implements Runnable{
        private DataInputStream dis;
        private DataOutputStream dos;
        private Socket client;
        private boolean isRunning;
        private String name;
        public Channel(Socket client) {
            this.client=client;
            try {
                dis=new DataInputStream(client.getInputStream());
                dos=new DataOutputStream(client.getOutputStream());
                isRunning=true;
                name=receive();  //获取名称
                this.send("欢迎来到聊天室");
                this.sendOthers(this.name+"进入了聊天室", true);
            } catch (IOException e) {
                System.out.println("服务端连接时出错");
                release();
            }

        }
        //接收消息
        private String receive() {
            String msg="";
            try {
                msg=dis.readUTF();
            } catch (IOException e) {
                System.out.println("服务端接收消息出错");
                release();
            }
            return msg;
        }
        //发送消息
        private void send(String msg) {
            try {
                dos.writeUTF(msg);
                dos.flush();
            } catch (IOException e) {
                System.out.println("服务端发送消息出错");
                release();
            }
        }
        //群聊:获取自己的消息发给其他人
        //私聊:约定数据格式为@***:msg
        private void sendOthers(String msg,boolean isSys) {
            boolean isPrivate=msg.startsWith("@");
            if (isPrivate) {  //私聊
                //获取目标数据
                int index=msg.indexOf(':');
                String uname=msg.substring(1, index);
                msg=msg.substring(index+1);
                for (Channel other : all) {
                    if (other.name.equals(uname)) {
                        other.send(this.name+"悄悄地对你说:"+msg);
                        break;
                    }
                }
            }else {
                for (Channel other:all) {
                    if (other==this) {
                        continue;
                    }
                    if (isSys) {
                        other.send(msg);  //系统消息
                    }else {
                        other.send(this.name+"对所有人说:"+msg);  //群聊消息
                    }
                }
            }
        }


        @Override
        public void run() {
            while (isRunning) {
                String msg=receive();
                if (!msg.equals("")) {
                    //send(msg);
                    sendOthers(msg,false);
                }
            }
        }
        //释放资源
        private void release() {
            isRunning=false;
            CloseUtils.close(dos,dis,client);
            //退出
            all.remove(this);
            sendOthers(this.name+"离开了聊天室。", true);
        }
    }
}
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.Socket;

/**
 * 在线聊天室:客户端
 * 目标:私聊
 * @author WHZ
 *
 */
public class Client {
    public static void main(String[] args) throws Exception {
        System.out.println("------client-----");
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        System.out.print("请输入用户名:");
        String name=br.readLine();
        //注意:获取用户名在建立连接之前!!!
        Socket client=new Socket("localhost", 8888);
        new Thread(new Send(client,name)).start();
        new Thread(new Receive(client)).start();
    }
}
import java.io.Closeable;
import java.io.IOException;
/**
 * 释放资源工具类
 * @author WHZ
 *
 */
public class CloseUtils {
    public static void close(Closeable... targets) {
        for (Closeable target:targets) {
            try {
                if (null!=target) {
                    target.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
import java.io.DataInputStream;
import java.io.IOException;
import java.net.Socket;

/**
 * 使用多线程封装接收端
 * @author WHZ
 *
 */
public class Receive implements Runnable{
    private Socket client;
    private DataInputStream dis;
    private boolean isRunning=true;
    public Receive(Socket client) {
        this.client=client;
        try {
            dis=new DataInputStream(client.getInputStream());
        } catch (IOException e) {
            System.out.println("客户端接收端出错");
            release();
        }
    }
    //接收消息
    private String receive() {
        String msg="";
        try {
            msg=dis.readUTF();
        } catch (IOException e) {
            System.out.println("客户端接收消息出错");
            receive();
        }
        return msg;
    }

    @Override
    public void run() {
        while (isRunning) {
            String msg=receive();
            System.out.println(msg);
        }
    }

    //释放资源
    private void release() {
        isRunning=false;
        CloseUtils.close(client,dis);
    }
}
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.util.Scanner;

/**
 * 使用多线程封装发送端
 * @author WHZ
 *
 */
public class Send implements Runnable{
    private Scanner sc;
    private Socket client;
    private DataOutputStream dos;
    private boolean isRunning=true;
    private String name;
    public Send(Socket client,String name) {
        this.client=client;
        this.sc=new Scanner(System.in);
        this.name=name;
        try {
            dos=new DataOutputStream(client.getOutputStream());
            send(this.name);  //发送名称
        } catch (IOException e) {
            System.out.println("客户端发送端出错");
            release();
        }
    }
    //发送消息
    private void send(String msg) {
        try {
            dos.writeUTF(msg);
            dos.flush();
        } catch (IOException e) {
            System.out.println("客户端发送消息出错");
            release();
        }
    }

    @Override
    public void run() {
        while (isRunning) {
            String msg=sc.nextLine();
            if (!msg.equals("")) {
                send(msg);
            }
        }
    }

    //释放资源
    private void release() {
        isRunning=false;
        CloseUtils.close(sc,client,dos);
    }
}

UDP通信的实现

UDP协议与上文提到的TCP协议不同,它是面向无连接的,对方不需要建立连接便可通信。UDP通信所发送的数据需要进行封包操作(使用DatagramPacket类),然后才能接收或发送(使用DatagramSocket类)。

DatagramPacket:数据容器(封包)

DatagramPacket类表示数据报包。数据报包用来实现封包的功能,其常用方法如下。

  • DatagramPacket(byte[] buf,int length):构造数据报包,用来接收长度为length的数据包。
  • DatagramPacket(byte[] buf,int length,InetAddress address):构造数据报包,用来接收长度为length的数据包发送到指定主机上的端口号。
  • getAddress():获取发送或接收方计算机的IP地址,此数据报将要发往该机器或者是从该机器接收到。
  • getData():获取发送或接收的数据。
  • setData(byte[] buf):设置发送的数据。

DatagramSocket:用于发送或接收数据报包

当服务器要向客户端发送数据时,需要再服务器端产生一个DatagramSocket对象,在客户端产生一个DatagramSocket对象。服务器端的DatagramSocket将DatagramPacket发送到网络上,然后被客户端的DatagramSocket接收。

DatagramSocket有两种常用的构造器,一种无须任何参数,常用于客户端;另一种需要指定端口,常用于服务器端:

  • DatagramSocket():用于构造数据报套接字,并将其绑定到本地主机上任何可用的端口。
  • DatagramSocket(int port):用于构造数据报套接字,并将其绑定到本地主机上的指定端口。

DatagramSocket类的常用方法:

  • send(DatagramPacket p):从此套接字发送数据报包。
  • receive(DatagramPacket p):从此套接字接收数据报包。
  • close():关闭此数据报套接字。

UDP通信编程基本步骤

  1. 创建客户端的DatagramSocket。创建时,定义客户端的监听端口。
  2. 创建服务器端的DatagramSocket。创建时,定义服务器端的监听端口。
  3. 在服务器端定义DatagramPacket对象,封装发送的数据包。
  4. 客户端将数据报包发送出去。
  5. 服务器端接收数据报包并解析。

以下是UDP通信的实际使用例子,以及一个咨询系统的简单实现:

UDP简单使用例子

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

/**
 * 基本流程:接收端
 * Address already in use: Cannot bind:同一个协议下端口不允许重复
 * 1.使用DatagramSocket 指定端口 创建接收端
 * 2.准备容器 封装成DatagramPacket包裹
 * 3.阻塞式接收包裹receive(DatagramPacket p)
 * 4.分析数据:byte[] getData() getLength()
 * 5.释放资源
 * @author WHZ
 *
 */
public class UdpServer {
    public static void main(String[] args) throws Exception {
        System.out.println("接收端启动中...");
        //1.使用DatagramSocket 指定端口 创建接收端
        DatagramSocket server=new DatagramSocket(9999);
        //2.准备容器 封装成DatagramPacket包裹
        byte[] container=new byte[1024*60];  //60KB
        DatagramPacket packet=new DatagramPacket(container,0,container.length);
        //3.阻塞式接收包裹receive(DatagramPacket p)
        server.receive(packet);  //阻塞式
        //4.分析数据:byte[] getData() getLength()
        byte[] datas=packet.getData();
        int len=packet.getLength();
        String str=new String(datas,0,len);  //将接收到的数据转换成字符串
        System.out.println(str);
        //5.释放资源
        server.close();
    }
}
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

/**
 * 基本流程:发送端
 * 1.使用DatagramSocket 指定端口 创建发送端
 * 2.准备数据 转为字节数组
 * 3.封装成DatagramPacket包裹,需要指定目的地
 * 4.发送包裹send(DatagramPacket p)
 * 5.释放资源
 * @author WHZ
 *
 */
public class UdpClient {
    public static void main(String[] args) throws Exception {
        System.out.println("发送端启动中...");
        //1.使用DatagramSocket 指定端口 创建发送端
        DatagramSocket client=new DatagramSocket(8888);
        //2.准备数据 转为字节数组
        String data="乾坤未定,你我皆是黑马!";
        byte[] datas=data.getBytes();
        //3.封装成DatagramPacket包裹,需要指定目的地
        DatagramPacket packet=new DatagramPacket(datas, datas.length,
                new InetSocketAddress("localhost",9999));
        //4.发送包裹send(DatagramPacket p)
        client.send(packet);
        //5.释放资源
        client.close();
    }
}

UDP传输基本数据类型

此处仅是一种例子,可以通过ByteArrayOutputStream和ByteArrayInputStream,传输各种数据。

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 基本类型:接收端
 * Address already in use: Cannot bind:同一个协议下端口不允许重复
 * 1.使用DatagramSocket 指定端口 创建接收端
 * 2.准备容器 封装成DatagramPacket包裹
 * 3.阻塞式接收包裹receive(DatagramPacket p)
 * 4.分析数据(将字节数组还原为对应的类型):byte[] getData() getLength()
 * 5.释放资源
 * @author WHZ
 *
 */
public class UdpTypeServer {
    public static void main(String[] args) throws Exception {
        System.out.println("接收端启动中...");
        //1.使用DatagramSocket 指定端口 创建接收端
        DatagramSocket server=new DatagramSocket(9999);
        //2.准备容器 封装成DatagramPacket包裹
        byte[] container=new byte[1024*60];
        DatagramPacket packet=new DatagramPacket(container, 0, container.length);
        //3.阻塞式接收包裹receive(DatagramPacket p)
        server.receive(packet);
        //4.分析数据(将字节数组还原为对应的类型):byte[] getData() getLength()
        byte[] datas=packet.getData();
//        int len=packet.getLength();
        DataInputStream bis=new DataInputStream(new  ByteArrayInputStream(datas));
        String string=bis.readUTF();
        int a=bis.readInt();
        boolean flag=bis.readBoolean();
        char c=bis.readChar();
        System.out.println(string+a+flag+c);
        //5.释放资源
        server.close();
    }
}
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;

/**
 * 基本类型:发送端
 * 1.使用DatagramSocket 指定端口 创建发送端
 * 2.将基本类型 转为字节数组
 * 3.封装成DatagramPacket包裹,需要指定目的地
 * 4.发送包裹send(DatagramPacket p)
 * 5.释放资源
 * @author WHZ
 *
 */
public class UdpTypeClient {
    public static void main(String[] args) throws Exception {
        System.out.println("发送端启动中...");
        //1.使用DatagramSocket 指定端口 创建发送端
        DatagramSocket client=new DatagramSocket(8888);
        //2.将基本类型 转为字节数组
        ByteArrayOutputStream baos=new ByteArrayOutputStream();
        DataOutputStream dos=new DataOutputStream(baos);
        dos.writeUTF("乾坤未定,你我皆是黑马!");
        dos.writeInt(666);
        dos.writeBoolean(false);
        dos.writeChar('v');
        byte[] datas=baos.toByteArray();
        //3.封装成DatagramPacket包裹,需要指定目的地
        DatagramPacket packet=new DatagramPacket(datas, datas.length, 
                new InetSocketAddress("localhost", 9999));
        //4.发送包裹send(DatagramPacket p)
        client.send(packet);
        //5.释放资源
        client.close();
    }
}

学生-教师咨询系统的设计与实现

限于篇幅不扩展推演,仅观看尝试理解其中原理即可,之后可能会出相关的一个系列讲解一下各种项目。

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

/**
 * 接收端:使用面向对象封装
 * @author WHZ
 *
 */
public class TalkReceive implements Runnable{
    private DatagramSocket server;
    private String name;
    public TalkReceive(int port,String name) {
        this.name=name;
        try {
            server=new DatagramSocket(port);  //指定端口创建接收端
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        while (true) {
            byte[] container=new byte[1024*60];  //创建接收容器
            DatagramPacket packet=new DatagramPacket(container, container.length);  //封装成DatagramPacket包裹
            try {
                server.receive(packet);  //阻塞式接收
                //分析数据
                byte[] datas=packet.getData();
                int len=packet.getLength();
                String msg=new String(datas,0,len);
                System.out.println(name+"说:"+msg);
                if (msg.equals("bye")) {
                    System.out.println(name+"已下线。");
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //关闭资源
        server.close();
    }

}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketException;

/**
 * 发送端:使用面向对象封装
 * @author WHZ
 *
 */
public class TalkSend implements Runnable{
    private DatagramSocket client;
    private String toIP;
    private int toPort;
    public TalkSend(int port,String toIP,int toPort) {
        this.toIP=toIP;
        this.toPort=toPort;
        try {
            client=new DatagramSocket(port);  //指定端口创建接收端
        } catch (SocketException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        while (true) {
            try {
                String msg=br.readLine();  //接收BufferRead传来的数据
                byte[] datas=msg.getBytes();  //将数据转为字节数组
                //封装成DatagramPacket包裹
                DatagramPacket packet=new DatagramPacket(datas, datas.length, new InetSocketAddress(toIP, toPort));
                //发送包裹
                client.send(packet);
                if (msg.equals("bye")) {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //释放资源
        client.close();
    }

}
public class TalkStudent {

    public static void main(String[] args) {
        System.out.println("学生端启动中...");
        new Thread(new TalkReceive(6666, "老师")).start();  //接收
        new Thread(new TalkSend(9999, "localhost", 7777)).start();  //发送
    }

}
public class TalkTeacher {

    public static void main(String[] args) {
        System.out.println("教师端启动中...");
        new Thread(new TalkSend(8888, "localhost", 6666)).start();  //发送
        new Thread(new TalkReceive(7777, "学生")).start();  //接收
    }

}

结语

本文至此结束,本篇文章主要就是讲解了如何使用Java网络编程实现TCP/UDP通信,并且配合例子理解。但是实战项目对于新手不建议直接观看,新手可以尝试理解实现原理,尝试使用封装使代码更加简洁。本文主要内容仅是讲通网络编程,想了解更多网络编程的相关知识可以翻阅相关书籍以及API文档,之后如果有时间会出一个系列专门讲解一下相关的实现项目。