某科学的超 Java网络编程:Socket通信原理及实例学习

首先来思考几个问题:

  • 如何把互联网上的网页抓下来?
  • 如何与互联网上的网络资源通信?
  • 如何在两个Java程序之间建立网络?
  • 面向连接与非面向连接的通信方式有什么区别?

接下来以此篇文章来学习:

  • 理解计算机网络编程的概念,掌握如何使用Java在一台或多台计算机之间进行基于TCP/IP协议的网络通讯。
  • 通过理解TCP/IP协议的通讯模型,以JDK提供的java.net包为工具,掌握各种基于Java的网络通讯的实现方法。

会涉及到的重难点:

  • 基于URL的网络编程(主要针对WWW资源)
  • 基于TCP的C/S网络编程(单客户、多客户)
  • 基于UDP的C/S网络编程


一. 网络编程基础

1. URL对象

此点介绍如何用Java访问外部资源,尤其是Java的URL对象来访问网络资源。

(1)网络基础知识:

  • IPv4地址(32位,4个字节),如:116.111.136.3;166.111.52.80

  • Ipv6地址(128位,16字节)

  • 主机名(hostname),如:www.baidu.com

比如 116.111.136.3,就是一个IP地址,随着互联网资源越来越多,原有的地址已经无法表示那么多资源,继而出现了Ipv6地址,它能够表示的地址范围更加宽广。又因为IP地址不容易记住,互联网出现一套机制,即主机名也叫域名,例如 www.baidu.com,它容易记住,背后也对应着一个IP地址。

  • 端口号(port number),如:80,21,1~1024位保留端口号

  • 服务类型(service):如http、telnet、ftp、smtp

当我们一台服务器或者一个IP地址在提供服务的时候,它可以提供多种服务,并且每种服务通过端口号来区分,例如我们通常访问外部资源使用的是80端口,进行ftp时使用的是21端口,发送邮件使用的是25端口。在整个端口号当中,1~1024为保留端口,一般在进行开发时最好不要使用这些端口。这个所谓的端口号就像是生活中银行的窗口,每个窗口为我们提供不同的服务,而这家银行就相当于服务器,在服务器中提供着多类型的服务,例如http、telnet、ftp。


了解以上网络基础概念后,思考如何获取互联网上的信息,举个例子抓取新浪网上的新闻信息,查看以下代码:

import java.io.*;
import java.net.*;

public class URLReader {

    public static void main(String args[]) throws Exception{
        URL url = new URL("http://www.sina.com/");
        BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));

        String inputLine;
        while ((inputLine = in.readLine()) != null)
            System.out.print(inputLine);
        in.close();
    }
}

以上类URLReader 功能为网络资源的读取器,首先传入域名来创建URL对象来表示网络资源,接着构造一个缓冲输入流,需要在其构造方法中传入参数,此参数就是URL对象的openStream方法返回的资源流,这样缓冲输入流就可获取到资源信息,接着一行行去读取并输出来。

(2)URL类(Uniform Resource Locator)定义
一切资源定位器的简称,它表示Internet上某一资源的地址。

(3)URL的组成:protocol:resourceName
协议名指明获取资源所使用的传输协议,如http、ftp、gopher、file等,资源名则应该是资源的完整地址,包括主机名、端口号、文件名,甚至是文件内部的一个引子

(4)构造URL对象方法

  • public URL(String spec)
    以网络资源字符串的形式作为参数传递给URL构造函数
URL urlBase = new URL("http://www.baidu.com");
  • public URL(URL context, String spec)
    可以用一个URL对象来表示上下文环境,例如以下例子,给一个资源名字,后面再跟一个超文本文件等等来构造一个URL对象。
URL gamelan = new URL("http://www.gamelan.com");
URL gamelanGames = new URL(gamelan, "Gamelan.game.html");
URL gamelanNetWork = new URL(gamelan, "Gamelan.net.html");
  • public URL(String protocol, String host, String file)
    通过字符串确定URL的访问协议,通过字符串确定主机名和文件名
new URL("http", "www.gamelan.com", "/pages/Gamelan.net.html");
  • public URL(String protocol, String host, int port, String file)
    此构造方法还可以确定端口号
new URL("http", "www.gamelan.com", 80, "/pages/Gamelan.net.html");

最后需要注意的是创建URL对象时需要进行try catch处理,主要是预防MalformedURLException异常,因为用户给出的URL地址很可能是不符合规范的。

(5)获取URL对象属性

Tables Are
public String getProtocol() 返回URL对象采用的协议
public String getHost() 返回URL对象的主机名
public String getPort() 返回URL对象的端口号
public String getFile() 返回URL对象采用的文件名
public String getRef() 返回URL对象的引用地址


2. URLConnection对象

(1)定义
一个URLConnection对象代表一个URL资源与Java程序的通讯连接,可以通过它对这个URL资源读或写。

(2)URLConnection与URL的区别

  • URL是单向的,只能去访问这个对象,读取器资源内容。URLConnection是双向的,还可以发送信息给目标资源。
  • URLConnection可已查看服务器的响应消息的首部
  • URLConnection可以设置客户端请求消息的首部

(3)使用URLConnection通信的一般步骤(按顺序):

  • 构造一个URL对象
  • 调用URL对象的 openConnection()方法获取对应该URL的URLConnection对象
  • 配置此URLConnection对象
  • 读取首部字段
  • 获得输入流读取数据
  • 获得输出流写入数据
  • 关闭连接

(4)实例展示
查看代码示例,通上个例子不同的是,此例采用URLConnection 对象来获取资源数据:

public class URLReader {

    public static void main(String args[]){
        try {
            URL url = new URL("http://www.sina.com/");
            URLConnection urlConnection = url.openConnection();

            BufferedReader in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null)
                System.out.print(inputLine);
            in.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


3. GET请求 和 POST请求

http中最常见的请求GETPOST可以来访问互联网上的资源,上节介绍的 URLConnection对象不仅可以获取资源,还可以发送一些信息到资源服务器上,将两者结合,首先来了解URLConnection编写GET请求:

(1)GET请求

  public static String sendGet(String url, String param){
        String result = "";
        BufferedReader in = null;
        try {
            String urlName = url + "?" +param;
            URL realUrl = new URL(urlName);
            //打开和URL的连接
            URLConnection urlConnection = realUrl.openConnection();
            //设置通用的请求属性
            urlConnection.setRequestProperty("accept", "*/*");
            urlConnection.setRequestProperty("connection", "Keep-Alive");
            //建立实际的连接
            urlConnection.connect();

            //定义BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));

            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭输入流
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

很显然,根据此方法的两个参数来构造最后的URL地址,根据URL对象获取到URLConnection对象,获取目标资源流并且设置了通用的请求属性,接着调用URLConnection对象的connect()方法与URL建立了实际的连接,后面就是那个缓存输入流对象将读取的数据存储到字符串result中,最后返回。


(2)POST请求
POST方法就是由java程序向服务器发送请求的时候先发送一些参数,然后服务器再响应本地并返回相应内容,此过程(即POST请求的参数)需要通过URLConnection的输出流来写入参数,查看以下实例:

    public static String sendPost(String url, String param){
        PrintWriter out = null;
        BufferedReader in = null;
        String result = "";

        try {
            URL readUrl = new URL(url);
            //打开与URL之间的连接
            URLConnection urlConnection = readUrl.openConnection();
            //设置通用的请求属性
            urlConnection.setRequestProperty("accept", "*/*");
            urlConnection.setRequestProperty("connection", "Keep-Alive");
            //允许输出流
            urlConnection.setDoOutput(true);
            //获取URLConnection对象对应的输出流
            out = new PrintWriter(urlConnection.getOutputStream());
            //发送请求参数
            out.print(param);
            //flush输出流的缓冲

            String line;
            while ((line = in.readLine()) != null) {
                result += line;
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //关闭输入流
            try {
                if(out != null){
                    out.close();
                }
                if(in != null){
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return result;
    }

POST请求与GET请求不同处从允许URLConnection对象的输出流开始,借助PrintWriter类,传入URLConnection对象对应的输出流作为参数获取PrintWriter对象,发送请求参数到服务器。接着等待服务器返回数据流,随之处理并存储到字符串中返回出去。(后续处理逻辑相同,不赘述)


(3)总结
以上两个例子是Java中典型的GET和POST请求,通过此例子可学习使用URLConnection发送请求。总之,在URLConnection的基础上提供了一系列针对http请求的内容,对Java网络编程部分起着重要作用,例如以下:

  • HTTP状态码(例如HTTP_OK:200)
  • setRequestMethod(设置请求方法GET、POST等)
  • getResponseCode(获取HTTP响应)




二. Socket学习

1. Socket解析

Socket通信原理即如何在两个Java程序之间建立网络连接,再了解此之前先熟悉一下TCP传输协议

(1)TCP(Transport Control Protocol)
面向连接的能够提供可靠的流式数据传输的协议。类似于生活中的打电话过程,通常拨完电话后会有一小段时间来建立连接,为了能够很好地进行语音传输。Java中有个类是使用TCP协议来进行网络通讯,例如:URL、URLConnection、Socket、ServerSocket。


(2)Socket通讯含义
网络上的两个程序通过一个双向的通讯连接实现数据的交换,这个双向链路的一端称为一个socket。socket通常用来实现客户端与服务端之间的连接。

某科学的超 Java网络编程:Socket通信原理及实例学习_第1张图片

查看上图,右侧的服务器可能提供多种类型的服务,例如http,通过端口80来提供服务。互联网上的用户通过网络连接到服务器上的http服务,而其它用户还可以连接到服务器的其它服务,例如SMTP(端口25),即发邮件的服务。

那么如何用Socket达到客户端和服务端连接的目的呢?


(3)Socket通讯原理

某科学的超 Java网络编程:Socket通信原理及实例学习_第2张图片

查看上图,客户端和服务器分别有一个Socket,由客户端向服务端发送一个Socket连接请求,服务器接收到后返回一个响应信号,这样两者之间建立了一个Socket连接。

某科学的超 Java网络编程:Socket通信原理及实例学习_第3张图片

上图是以代码的角度来讲解:在服务器有一个ServerSocket类的对象一直在运行着,等待客户端是否发起了请求,当它收到请求后ServerSocket会调用方法创建并返回一个Socket对象,此对象就是用来与客户端进行对等连接。当客户端和服务器建立Socket连接后,下一步就是传送数据,即通过Socket对象获取双方的输入输出流进行读写。

举个例子,当客户端发一个数据到服务器,接收到后再发送一个数据给客户端,此过程实际就是IO读写的方式,当网络建立起来后,所谓网络通讯就是IO读写。当两者皆完成了读写目的后,下一步则调用Socket的close方法进行关闭,此过程结束。



2. Socket代码实例

接下来以代码实际例子来实现Socket通讯,而Java中正有一个类为Socket,来学习此类使用.

(1)Socket创建

  • Socket()

  • Socket(InetAddress address, int port)
    InetAddress 代表需要构造Socket远程目标的连接地址,port则是连接目标对象的端口号。

  • Socket(String host, int port)
    同上一个构造方法类似,只是主机名由字符串形式表示。

  • Socket(InetAddress host, int port, InetAddress localAddr, int localPort)
    后两个参数代表本机的地址和端口号。

  • Socket(String host, int port, InetAddress localAddr, int localPort)
    含义相同,只是第一个参数主机名由字符串形式表示。


(2)客户端与服务器的Socket创建

客户端Socket的建立

try{
    Socket socket = new Socket("127.0.0.1", 2000);
}catch(IOException e){
    System.out.println("Error:"+e);
}

创建Socket的第一个参数代表目标服务器的地址,而以上展示的 127.0.0.1 通常指本机,因为在调试程序时只有一台电脑,用它来同时启动两个虚拟机来互相连接,第二个参数是端口号,原则上不取1024以下的保留端口号即可,注意客户端与服务器端口号应一致。

服务器Socket的建立

        ServerSocket serverSocket = null;

        try {
            serverSocket = new ServerSocket(2000);
        }catch (Exception e){
            System.out.print("can not listen to :" + e);
        }

        Socket socket = null;
        try {
            socket = serverSocket.accept();
        }catch (Exception e){
            System.out.print("Error :" + e);
        }

可以看到服务端首先构造的并非是Socket 对象,而是ServerSocket 对象,传入构造方法中的参数就是端口号,同需连接客户端的端口号一致。接下来通过ServerSocket 对象的accept()方法来获取Socket对象,此方法被称为阻塞方法,因为它一直在运行,等待客户端发送的Socket连接请求,若未收到请求,accept()方法就一直在循环执行,始终不返回结果,直到收到请求后,accept()方法会返回发送请求的Socket 对象。构造完客户端与服务端的Scocket对象后,接下来可以进行网络通讯了。

输入流与输出流
在通讯之前还需要利用Socket对象来构建输入输出流,代码如下所示:

【方式一:】
            PrintStream outputStream = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
            DataInputStream inputStream = new DataInputStream(socket.getInputStream());

【方式二:】
            PrintWriter output = new PrintWriter(socket.getOutputStream(), true);
            BufferedReader input = new BufferedReader(new InputStreamReader(socket.getInputStream()));

输出流重点:通过Socket对象获取到输出流,先用BufferedOutputStream将其封装一层,外面再用PrintStream包一层,这样可以利用PrintStream来进行通讯。输出流的主要作用就是往外发送消息。

输入流重点:通过Socket对象获取到输出流,紧接着在外面包装一层DataInputStream ,从而获得DataInputStream 对象。输入流的主要作用就是接收来自另外一方的数据。方法二中的输入流,首先InputStreamReader 将字节流转换为字符流,BufferedReader 流能够读取文本行 , 通过向 BufferedReader 传递一个 Reader 对象 , 来创建一个 BufferedReader 对象 。


(3)简单聊天实例
以下以一个简单的例子——命令行聊天程序,来实践以上讲解的知识点。

客户端

【客户端 TalkClient】

public class TalkClient{
    public static void main(String args[]) throws IOException {
        Socket socket = new Socket("127.0.0.1", 4700);
        BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
        BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        String readline = inSystem.readLine();
        while (!readline.equals("bye")) {
            outputStream.println(readline);
            outputStream.flush();
            System.out.println("Client:" + readline);
            System.out.println("Server:" + inputStream.readLine());
            readline = inSystem.readLine();
        }
        outputStream.close();
        inputStream.close();
        socket.close();
    }
}

首先构造Socket对象,再依次构建三个流,分别是一个输入流来获取键盘输入,再构造一个输出流将信息发送给对方网络,最后构造一个输入流获取响应的信息。接着读取键盘输入,判断内容若不是“bye”则将信息发送给对方,再接收来自对方的信息,直到读取键盘输入“bye”时关闭输入输出流与socket连接。

服务端

【服务端 TalkServer 】
public class TalkServer {
    public static void main(String args[]) throws IOException {
        ServerSocket serverSocket = null;
        Socket socket = null;
        boolean listening = true;
        try {
            serverSocket = new ServerSocket(4700);
            socket = serverSocket.accept();
        } catch (Exception e) {
            e.printStackTrace();
        }

        String line ;
        BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
        BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

        System.out.println("Client"+ inputStream.readLine());
        line = inSystem.readLine();
        while (line.equals("bye")){
            outputStream.println(line);
            outputStream.flush();
            System.out.println("Server"+ line);
            System.out.println("Client"+ inputStream.readLine());
            line = inSystem.readLine();
        }
        outputStream.close();
        inputStream.close();
        socket.close();
    }
}

首先构造一个ServerSocket对象,通过服务端server的accept 方法来获取客户端发送的socket请求,该方法会返回socket对象,接下来需要站在服务端的角度进行通讯。还是照例构造三个流,含义不再赘述,接着后续逻辑与客户端相同。


(4)实例总结
服务端与客户端的例子很大部分内容相同,只是服务端需要构造一个ServerSocket,通过ServerSocket得到和客户端连接的Socket对象,得到对象后可以构造输入、出流进行IO流的读写,最后关闭流、Socket即可。所以网络编程到最后演化成UI编程,呈现出的效果:

Clent: hello!
Server:hey
Clent: how are you
Server:i am ok
...
Clent: bye
Server:bye

(最后需要注意的是想要实现以上效果,需先启动一个虚拟机运行服务端程序,再启动第二个虚拟机运行客户端程序。由于只是一个简单程序,所以两端之间只能一句一句互相说,可自行扩展)





三. Socket通讯进阶

下面会着重介绍多个程序之间进行通讯甚至是广播的知识点,首先来思考几个问题:

  • Java中的Socket多客户端机制如何实现?
  • 数据报是如何通信的?
  • 广播通信如任何实现?

1. Socket多客户端通信实现

(1)多客户机制原理

某科学的超 Java网络编程:Socket通信原理及实例学习_第4张图片

通过一个例子来解释多客户机制,在服务端有一个ServerSocket一直在等待客户端发送请求。如图所示,ClientA发送请求到服务端,此时服务端实例化一个线程来处理与ClientA聊天相关的事,ClientB、ClientC也是如此。


(2)Socket多客户端例子

客户端

【客户端代码与第二节的简单聊天例子中的客户端TalkClient类完全相同,在此不重复贴】

服务端

【服务端 MultiTalkServer 】

public class MultiTalkServer {
    static int clientNum = 0;
    public static void main(String args[]) throws IOException {
        ServerSocket serverSocket = null;
        boolean listening = true;
        try {
            serverSocket = new ServerSocket(4700);
        }catch (Exception e){
            e.printStackTrace();
        }
        while (listening){
            new ServerThread(serverSocket.accept(), clientNum).start();
            clientNum++;
        }
        serverSocket.close();
    }

    public static class ServerThread extends Thread{
        Socket socket = null;
        int clientNum;

        public ServerThread(Socket socket, int num) {
            this.socket = socket;
            clientNum = num + 1;
        }

        public void run(){
            try {
                String line ;
                BufferedReader inSystem = new BufferedReader(new InputStreamReader(System.in));
                PrintWriter outputStream = new PrintWriter(socket.getOutputStream());
                BufferedReader inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream()));

                System.out.println("Client"+ clientNum + ":" + inputStream.readLine());
                line = inSystem.readLine();
                while (line.equals("bye")){
                    outputStream.println(line);
                    outputStream.flush();
                    System.out.println("Server"+ line);
                    System.out.println("Client"+ clientNum + ":" + inputStream.readLine());
                    line = inSystem.readLine();
                }
                outputStream.close();
                inputStream.close();
                socket.close();

            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

服务端 类中有个静态成员变量clientNum用来记录发起请求的客户端数量,构造ServerSocket对象,在while循环方法体创建ServerThread线程。相当于main 方法一直在循环等待客户端socket请求,一旦接收到socket请求,就实例化一个线程与之交互聊天,线程记录数量加一。

来看ServerThread实现,内部维护了socket对象和客户端数量,run 方法中首先创建了三个流,意义与客户端相同,其实已经很是熟悉其中概念了,用一个输入流获取键盘输入,再用输出流将信息发送给对方网络,最后构造一个输入流获取响应的信息。同客户端相同,接下来判断键盘输入文本,为“bye”则停止发送,聊天结束;否则继续读取键盘输入流发送信息,最后关闭流和socket的连接。

注意:
(以上是展示的例子,如要运行查看效果,需先启动一个虚拟机运行服务端程序,再启动第二个虚拟机运行客户端程序,从而可以启动第三、四个去运行客户端程序,查看一个服务端同时与多个客户端进行聊天的效果)



2. 数据报通信

此节将讲解数据报通信的原理,即不需要面向连线的通信方式,数据报通信方式采用的是UDP(User Datagram Protocol)协议,之前介绍过,这里同TCP作比较再次介绍。

(1)UDP与TCP学习

  • UDP(User Datagram Protocol)
    非面向连接的提供不可靠的数据包式的数据传输协议。类似于从邮局发送信件的过程,发送信件是通过邮局系统一站一站进行传递,中间也有可能丢失。Java中有些类是基于UDP协议来进行网络通讯的,有DatagramPacket、DatagramSocket、MulticastSocket等类。

  • TCP(Transport Control Protocol)
    面向连接的能够提供可靠的流式数据传输的协议。类似于打电话的过程,在拨完电话后两者之间会先建立连接,为了更好的通话,确保通话连接后两者开始互相传输信息。相对应的类有URL、URLConnection Socket、ServerSocket等

    UDP 与 TCP的区别

  • TCP有建立时间,UDP无

  • UDP传输有大小限制,每一个数据报需在64K以内
  • TCP常见应用:Telnet远程登录、Ftp文件传输
  • UDP常见应用:ping指令,用来检测网络上某台服务器是否还在提供服务。

(2)数据报学习

构造数据报的通信使用到的类

  • DatagramSocket()
  • DatagramSocket(int port)
  • DatagramPacket(byte ibuf[], int ilengh) //接收
  • DatagramPacket(byte ibuf[], int ilengh, InetAddress iaddr, int port) //发送

DatagramSocket实际上是一种数据报通信的Socket,构造它时可指定端口号。需要发送的数据存放在DatagramPacket对象中,第一种构造方法的第一个参数类型是字节数组,用来接收数据报,第二个参数即数据报的长度。第二种构造方法中的前两个参数意义相同,后两个个参数是指数据报被发送到的目标地址以及对应的端口号。

  • 接收数据报
DatagramPacket packet = new DatagramPacket(buf, 256);
socket.receive(packet);
  • 发送数据报
DatagramPacket packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);

(3)数据报发送与接收实例

QuoteClient类主要目的是想服务器询问某些股票的信息,而服务端按照本地文件虚拟响应数据返回给客户端。

客户端

【客户端 QuoteClient 】

public class QuoteClient {

    public static void main(String[] args)throws IOException{
        if(args.length != 1){
            System.out.println("Usage:java QuoteClient ");
            return;
        }
        DatagramSocket socket = new DatagramSocket();

        //发送请求
        byte[] buf = new byte[256];
        InetAddress address = InetAddress.getByName(args[0]);
        DatagramPacket packet = new DatagramPacket(buf, buf.length, address, 4445);
        socket.send(packet);

        //获取响应
        packet = new DatagramPacket(buf, buf.length);
        socket.receive(packet);

        //显示响应数据
        String received = new String(packet.getData());
        System.out.println("Quote of the Moment:"+ received);
        socket.close();
    }

}

QuoteClient类中main 方法中第一行if判断参数args是否等于1,意味着程序在执行时必须跟一个参数,即目标服务器的主机名,若无会打印Usage:java QuoteClient 提示。接下来依次构造Socket对象和数据报 ,这个目标地址InetAddress就是main 方法中的参数args[0],再构造一个数据报发送。发送结束后想要接收到服务端的回信,即包含了客户端想要的股票信息。最后关闭socket连接。

服务端

【服务端 QuoteServer 】

public class QuoteServer {

    public static void main(String[] args)throws IOException{
        new QuoteServerThread().start();        
    }

    public static class QuoteServerThread extends Thread {
        protected DatagramSocket socket = null;
        protected BufferedReader in = null;
        protected boolean moreQuotes = true;

        public QuoteServerThread() throws IOException{
            this("QuoteServerThread");
        }

        public QuoteServerThread(String name) throws IOException{
            super(name);
            socket = new DatagramSocket(4445);
            try{
                in = new BufferedReader(new FileReader("one-lines.txt"));
            }catch(FileNotFoundException e){
                e.printStackTrace();
            }
        }

        public void run(){
            while(moreQuotes){
                try{

                    byte[] buf = new byte[256];
                    DatagramPacket packet = new DatagramPacket(buf, buf.length);
                    socket.receive(packet);
                    String dString = null;
                    if(in == null){
                        dString = new Date().toString();
                    }else{
                        dString = getNextQuote();
                    }
                    buf = dString.getBytes();

                    //返回数据给客户端(address + port)
                    InetAddress address = packet.getAddress();
                    int port = packet.getPort();
                    packet = new DatagramPacket(buf, buf.length, address, port);
                    socket.send(packet);

                }catch(Exception e){
                    e.printStackTrace();
                    moreQuotes = false;
                }
            }
            socket.close();
        }

        private String getNextQuote() {
            String returnValue = null;
            try{
                if((returnValue = in.readLine()) == null){
                    in.close();
                    moreQuotes = false;
                    returnValue = "NO more quotes. Goodbye!";
                }

            }catch(Exception e){
                e.printStackTrace();
            }
            return returnValue;
        }

    }
}

服务端QuoteServer类main 方法开启了一个线程,首先来看线程QuoteServerThread的构造方法,主要是创建Socket,构造一个文件输入流,因为这是在模仿服务端发送信息到客户端,所以将股票信息写到此文件中,这样每次有客户端发送请求咨询股票价格时,就从文件读出对应股票的价格返回给客户端。再看run方法,首先一个While循环代表文件中信息未读完的话一直读取,在接收客户端发来的接收包后发送回信,判断服务端的文件内容是否为空,是则返回客户端当前日期,否则获取下一条股票信息将其返回。注意返回信息的前提是构造一个数据报包,需要知道客户端的地址和端口号,而正好利用接收到客户端发来的数据报包,通过数据报包对象来获取地址和端口号。


(4)例子总结
通过以上例子可以得出结论,整个过程非常类似于生活中互相写信的通讯方式。客户端构造一个DatagramPacket对象,填充一些信息,通过DatagramSocke对象t的send() 方法发送到服务端。服务端接收到数据报包后,同时也得知了客户端的地址和端口号,服务端也构造一个DatagramPacket数据报,填充响应的数据返回给客户端。



3. 数据报进行广播通信

其实利用数据报包可进行广播通讯,只适用于小范围局域网。之前介绍的DatagramSocket 只允许存放一个目的地址,但是MulticastSocket 类可以把数据报以广播的形式发送到所有监听该端口的客户端。MulticastSocket 在客户端使用,来监听服务器广播来的数据。下面通过一个实例来学习:

【客户端 MulticastClient】
public class MulticastClient {
    public static void main(String args[]) throws IOException{
        MulticastSocket multicastSocket = new MulticastSocket(4446);
        InetAddress address = InetAddress.getByName("230.0.0.1");
        multicastSocket.joinGroup(address);
        DatagramPacket packet;

        //获取接收数据
        for(int i=0; i<5; i++){
            byte[] buf = new byte[255];
            packet = new DatagramPacket(buf, buf.length);
            multicastSocket.receive(packet);
            String received = new String(packet.getData());
            System.out.println("Quote of the Moment:"+received);
        }
        multicastSocket.leaveGroup(address);
        multicastSocket.close();
    }
}

首先构造一个广播socket对象,同时需要传入端口号到构造方法,接着构造一个IP地址,调用广播socket对象的joinGroup 方法将socket对象加入一个组,即IP地址所标识的组。接下来以for循环来模拟客户端接收广播信息的过程。最后循环5次接收数据后调用广播socket对象的 leaveGroup 的方法,即离开当初关注的那个组,然后关闭socket连接。




以上是Java网络编程着重于Socket的学习笔记,中间加述了大多例子来实践综合了Socket知识点,但这篇文章并不代表记录了全部的知识点,只是尽我所能来记录现有可掌握到的知识。如有错误,望指正~

希望对你们有帮助:)

你可能感兴趣的:(Java学习笔记)