Java 网络之 TCP 编程

Java 实现计算机网络的通信就离不开 socket,socket 意为套接字可以理解为计算机之间通信的一个管道,一台计算机要向另一台计算机发送信息,首先将信息放到连接这两台计算机的管道,通过管道将信息传递给另一台计算机。同样的,要收取信息,就从管道中取出另一台计算机放到管道中的信息。因此,计算机在通信之前,首先要建立套接字连接到两台计算机上。

在讲解计算机通信的过程之前,首先认识几个常用的术语:

端口(Port):每个应用都有一个唯一的端口号,它用于区别不同的应用程序。计算机收到其他计算机的某个应用程序的信息要传给本机上对应的某个特定应用程序,就是根据端口号来正确地传达。端口号的范围是 0~65535,其中 0~1023 为系统保留。因此,如果要自定义端口号,最好使用 1024 及以后的端口号。一些常见的程序默认端口号有:HTTP(80)FTP(21)Telnet(23)

套接字(Socket):由 IP 地址和端口号组成,用于在要进行通信的计算机之间建立连接。

Java 中提供网络功能的类有四个:InetAddressURLSocketDatagram

InetAddress:用于标识网络中的文件资源,表示 IP 地址信息。

URL:统一资源定位符,通过 URL 可以直接读取或写入网络上的数据。

Socket:基于 TCP 的网络通信类。

Datagram:基于 UDP 的网络通信类,将数据保存在数据报中发送。

接下来看看各个类是怎么使用的。

InetAddress

 

通过查看 JDK_API 可以看到,InetAddress 类位于 java.lang 包下。

他没有构造方法,但是他有一些 静态方法static)可以返回 InetAddress 对象。

这样就可以通过静态方法的 类名.方法 调用静态方法得到 InetAddress 对象,再调用 InetAddress 对象的其他方法。

上面分别通过静态方法 getLocalHost()getByName(计算机名)getByName(IP地址) 获得 InetAddress 对象。然后通过这个对象的方法得到资源的信息。最后是试验了一下 bytes.toString() 方法和 Arrays.toString(bytes) 方法的区别。

URL

 

URL 类主要用于通过 URL(统一资源定位符)获得网络上的资源,然后对网络上资源进行数据读取和写入的操作。查看 JDK_API 可以看到 URL 类在 java.net 包下

它提供了一些构造方法,用户创建不同类型的 URL 对象。

首先看 URL 类的基本方法的使用,可以用来进行 url 有关的什么样的操作。

首先使用传入 url 字符串的方式构造 URL 对象,接着使用已有的 URL 对象构造了新的 URL 对象(也就是使用上下文重新构造了 URL 对象)。可以看到 getPort()getDefaultPort() 方法的区别。如果没有指定端口号,getPort() 方法返回 -1,而 getDefaultPort() 返回默认端口。可以很容易从结果中看到对应的方法获得什么样的信息。

为了更直观的体会 URL 对象可以直接读取和写入网络上的数据,可以进行以下操作。

可以看到,通过 URL 对象的 openStream() 方法,可以获得从该连接读入的 InputStream ,这是一个字节输入流,接着将它包装成字符输入流,并指定编码信息,否则可能出现乱码,然后将字符输入流进行缓冲(图片上有一点错误,字节流应为字符流),最后从缓冲区每次读取一行,循环读取,从而获得网站链接的源码信息。

其中 openStream() 的 API 为:

 InputStream   openStream()           打开到此 URL 的连接并返回一个用于从该连接读入的 InputStream

 

Socket  

 

Socket 类就是基于 TCP 实现网络通信的类,TCP 进行的是面向连接的,可靠字节流通信。实现 TCP 通信主要涉及两大类:客户端的 Socket 类和 服务器端的 ServerSocket 类。

在使用 TCP 的方式进行通信之前,首先要了解服务器和客户端通信的过程。

之前已经说过,要进行通信首先要建立 socket 对象。服务器端首先建立监听 socket 然后调用 accept() 方法来监听等待客户端的连接,客户端建立连接 socket 向服务器发送连接请求,服务器收到请求创建连接 socket,这样服务器和客户端就建立了连接,客户端和服务器都利用利用输入输出流 OutputStreamInputStream 发送和接收数据,通信完成,关闭连接,释放资源。

由上面的通信过程可以总结出客户端和服务器通信的流程:

因此,需要创建进行通信的客户端类和服务器端类 Server.javaClient.java

服务器端程序 Server.java 为:

 //Server.java 
 package com.javaweb;

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.net.ServerSocket;
 import java.net.Socket;

 import javax.sound.midi.MidiDevice.Info;
 import javax.swing.text.AbstractDocument.BranchElement;

 public class Server {

     public static void main(String[] args) {
         try {
            ServerSocket server = new ServerSocket(8888);
            //侦听并接受到此套接字的连接,当客户端连接到服务器,返回一个 socket 对象
            System.out.println("服务器已经启动,等待客户端的连接...");
            Socket socket = server.accept();
            //获得套接字的输入流
            InputStream is = socket.getInputStream();
            //将套接字的字节输入流转化为字符输入流
            InputStreamReader isr = new InputStreamReader(is);
            //为字符输入流添加缓冲
            BufferedReader br = new BufferedReader(isr);
            String info = br.readLine();
            while (info!=null) {
                System.out.println("我是服务器,我收到了客户端的信息:"+info);
                info = br.readLine(); //每次读取一行              
            }
            socket.shutdownInput();
            //获取套接字的字节输出流
            OutputStream oStream = socket.getOutputStream();
            //将套接字的字节输出流包装成打印流
            PrintWriter pWriter = new PrintWriter(oStream);
            pWriter.write("我是服务器发送的消息,欢迎客户的登录");
            pWriter.flush();

            pWriter.close();//关闭打印流
            oStream.close();//关闭字节输出流
            br.close();//关闭缓冲资源
            isr.close();//关闭字符输入流资源
            is.close();//关闭字节输入流资源
            socket.close();//关闭套接字资源
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

 }

客户端程序 Client.java 为 

 //Client.java
 package com.javaweb;

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.net.Socket;
 import java.net.UnknownHostException;
 import java.util.Locale;

 public class Client {

    public static void main(String[] args) {
        try {
            //创建流套接字并将其连接到指定主机的指定端口
            Socket socket = new Socket("localhost",8888);
            //获取套接字的字节输出流
            OutputStream os = socket.getOutputStream();
            //将字节输出流转换成打印流
            PrintWriter pw = new PrintWriter(os);
            //在字符输出流中写入字符
            pw.write("用户名:Tom,密码:123456");
            pw.flush();//将打印流中的数据发送出去
            //获取套接字的字节输入流
            socket.shutdownOutput();
            InputStream iStream = socket.getInputStream();
            //将字节输入流包装成字符输入流
            InputStreamReader iReader = new InputStreamReader(iStream);
            //将字符输入流的数据写入缓冲
            BufferedReader br = new BufferedReader(iReader); 
            String info = br.readLine();//读取一行
            while (info!=null) {
                System.out.println("我是客户端,我收到服务器端的消息:"+info);
                info = br.readLine();
            }

            br.close();//关闭缓冲区资源
            iReader.close();//关闭字符输入流
            iStream.close();//关闭字节输入流
            pw.close();//关闭打印流
            os.close();//关闭字节输出流
            socket.close();//关闭socket资源
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } 

    }

 }

客户端向服务器发送了用户名和密码的字符串,服务器收到信息并响应客户端欢迎客户端的登录,客户端收到服务器发来的响应并打印。

先启动服务器程序:

然后启动客户端程序,向服务器发送信息。

这时客户端发送信息完成后就接收到了服务器传来的响应信息,再看服务器是否接受到客户端发送的信息。

服务器正确接收了客户端发来的消息,完成通信过程。

但是这个通信程序有一个缺点就是这个个服务器只能接受一个客户端的连接,accept() 后调用便阻塞,等待客户端连接,连接以后便一直为这个客户服务,直到处理完该客户的任务关闭连接,才再和其他客户端进行连接。这个不足可以通过利用多线程实现多客户端的连接,一个服务器可以同时和多个客户端进行连接。他的实现思路是:

服务器一直处于监听状态,收到客户端的连接请求就创建一个线程和客户端连接,由这个线程处理客户端需要完成的任务。

因此我们需要一个服务器线程的类,创建线程专门用于处理客户的任务。

服务器线程程序 ServerThread.java 为:

 //ServerTnread.java
 package com.javaweb;

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.net.Socket;

 public class ServerThread extends Thread{
    Socket socket = null;
    public ServerThread(Socket socket) {
        this.socket = socket;
    }

    public void run(){
        //获得套接字的输入流
        InputStream is = null;
        InputStreamReader isr = null;
        BufferedReader br = null; 
        OutputStream oStream = null;
        PrintWriter pWriter = null;
        try {
            is = socket.getInputStream();
            isr = new InputStreamReader(is);
            br = new BufferedReader(isr); 

            String info = br.readLine();
            while (info!=null) {
                System.out.println("我是服务器,我收到了客户端的信息:"+info);
                info = br.readLine(); //每次读取一行              
            }
            socket.shutdownInput();
            //获取套接字的字节输出流
            oStream = socket.getOutputStream();
            //将套接字的字节输出流包装成打印流
            pWriter = new PrintWriter(oStream);
            pWriter.write("我是服务器发送的消息,欢迎客户的登录");
            pWriter.flush();            

        } catch (IOException e) {
            e.printStackTrace();
        }
        finally{ //资源的善后工作都放在 finally 语句块中,不管是否异常都要执行
            pWriter.close();//关闭打印流
            try {
                oStream.close(); //关闭字节输出流
                br.close();      //关闭缓冲资源
                isr.close();     //关闭字符输入流资源
                is.close();      //关闭字节输入流资源
                socket.close();  //关闭套接字资源
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 }

这时服务器程序 Server.java 就可以写为:

 //Server.java
 package com.javaweb;

 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
 import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;

 import javax.sound.midi.MidiDevice.Info;
 import javax.swing.text.AbstractDocument.BranchElement;

 public class Server {
    public static void main(String[] args) {    
            ServerSocket server = null;
            InetAddress address = null;
            int count = 0; //统计连接到服务器的客户数目
            try {
                server = new ServerSocket(8888);
                //侦听并接受到此套接字的连接,当客户端连接到服务器,返回一个 socket 对象
                System.out.println("服务器已经启动,等待客户端的连接...");
                while(true){ //循环监听,如果不循环,收到客户端连接请求后就不会再监听,就只能连接一个客户端
                    Socket socket = server.accept();
                    count++;
                    address = socket.getInetAddress(); //返回套接字连接的地址,是 InetAddress 类
                    System.out.println("收到第"+count+"位客户的连接");
                    System.out.println("套接字连接地址为:"+address.getHostAddress());
                    //获取套接字绑定的本地地址
                    System.out.println("套接字绑定的本地地址为:"+socket.getLocalAddress());
                    ServerThread sThread = new ServerThread(socket); //创建服务器线程

                    sThread.start(); //启动线程,处理客户端请求
                }       
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
    }
 }

重新启动客户端,然后启动多个客户端,服务器可以同时和多个客户端连接,每个客户端都可以收到服务器的响应。服务器的打印结果为:

可以看到,服务器同时连接了2个客户端,当然也可以连接更多的客户端,通过 socket 的 getIntAddress() 方法(返回 InetAddress 类的对象)或者 socket 自己的方法,可以获得用于连接的网络参数信息,比如 IP 地址,主机名等等。





评论

  1. #1

    dUoxKpta 2019-09-06 01:15:04
    dUoxKpta

  2. #2

    WEmXeAbt 2019-09-05 22:19:21
    WEmXeAbt

  3. #3

    zuLdJdBo 2019-09-05 19:41:59
    zuLdJdBo