Java网络编程——Socket用法解析

在客户/服务器通信模式中,客户端需要主动创建与服务器连接的Socket,服务器端收到了客户的连接请求,也会创建与客户连接的Socket。Socket可以被看作是通信连接两端的收发器,服务器与客户都通过套接字来收发数据。

1、构造Socket

Socket的构造方法有以下几种重载形式:

    Socket();
    Socket(InetAddress address, int port) throws UnknownHostException, IOException;
    Socket(InetAddress address, int port, InetAddress localAddr, int localPort) throws IOException;
    Socket(String host, int port)throws UnknownHostException, IOException;
    Socket(String host, int port, InetAddress localAddr, int localPort)throws IOException;;
    Socket(Proxy proxy);

除了第1个不带参数的构造方法,其他构造方法都会试图建立与服务器的连接,如果连接成功,就返回Socket对象;如果因为某些原因连接失败,就会抛出IOException。

下面的PortScanner类能够扫描主机上从1到1024之间的端口,判断这些端口是否已经被服务器程序监听。PortScanner类的scan()方法在一个for循环中创建Socket对象,每次请求连接不同的端口,如果Socket对象创建成功,就表明在当前端口有服务器程序监听。

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

/**
 * @title PortScanner
 * @description 测试
 * @author: yangyongbing
 * @date: 2023/12/5 12:03
 */
public class PortScanner {
    public static void main(String[] args) {
        String host="localhost";
        if(args.length>0){
            host=args[0];
        }
        new PortScanner().scan(host);
    }

    public void scan(String host){
        Socket socket=null;
        for(int port=1;port<1024;port++){
            try {
                socket=new Socket(host,port);
                System.out.println("There is a server on port "+port);
            }catch (IOException e){
                System.out.println("Can't connect to port "+port);
            }finally {
                if(socket!=null){
                    try {
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}

在这里插入图片描述

1.1、设定等待建立连接的超时时间

当客户端的Socket构造方法请求与服务器连接时,可能要等待一段时间。在默认情况下,Socket构造方法会一直等待下去,直到连接成功,或者出现异常。Socket构造方法请求连接时,受底层网络的传输速度的影响,可能会处于长时间的等待状态。如果希望限定等待连接的时间,那么该如何做呢?此时就需要使用第1个不带参数的构造方法。
在这里插入图片描述
以上SocketAddress类表示一个套接字的地址,它同时包含了IP地址和端口信息。以上代码用于连接到本地机器上的监听8000端口的服务器程序,等待连接的最长时间为1分钟。如果在1分钟内连接成功,则connect()方法顺利返回;如果在1分钟内出现某种异常,则抛出该异常;如果在1分钟后既没有连接成功,也没有出现异常,那么会抛出SocketTimeoutException。Socket类的connect(SocketAddress endpoint,int timeout)方法负责连接服务器,参数endpoint是指定服务器的地址,参数timeout是设定的超时时间,以ms为单位。如果参数timeout被设为0,则表示永远不会超时。

不带参数的构造方法的另一个作用是在连接服务器之前,设置Socket的选项。

1.2、设定服务器的地址

除了第1个不带参数的构造方法,其他构造方法都需要在参数中设定服务器的地址,包括服务器的IP地址或者主机名,以及端口。
在这里插入图片描述
InetAddress类表示主机的IP地址,InetAddress类提供了一系列静态工厂方法,用于构造自身的实例,例如:
在这里插入图片描述

1.3、设定客户端的地址

在一个Socket对象中,既包含远程服务器的IP地址和端口信息,也包含本地客户端的IP地址和端口信息。在默认情况下,客户端的IP地址来自客户程序所在的主机,客户端的端口则由操作系统随机分配。Socket类还有两个构造方法允许显式地设置客户端的IP地址和端口。
在这里插入图片描述

如果一个主机同时属于两个以上的网络,它就可能拥有两个以上IP地址,例如一个主机在Internet网络中的IP地址为“222.67.1.34”,在一个局域网中的IP地址为“112.5.4.3”。假定这个主机上的客户程序希望和同一个局域网上的一个服务器程序(地址为:112.5.4.45:8000)通信,客户端可按照如下方式构造Socket对象。
在这里插入图片描述

1.4、客户连接服务器时可能抛出的异常

当Socket的构造方法请求连接服务器时,可能会抛出以下异常:

  • UnknownHostException:如果无法识别主机的名字或IP地址,就会抛出这种异常。
  • ConnectException:如果没有服务器进程监听指定的端口,或者服务器进程拒绝连接,就会抛出这种异常。
  • SocketTimeoutException:如果等待连接超时,就会抛出这种异常。

以上4种异常都是IOException的直接或间接子类,如下图所示:
在这里插入图片描述
下面以ConnectTester类为例,介绍抛出各种异常的原因。ConnectTester接收用户从命令行输入的主机名和端口,然后连接到该地址,如果连接成功,就会计算建立连接所花的时间;如果连接失败,就会捕获各种异常。下面是ConnectTester类的源程序。
在这里插入图片描述
在控制台中运行命令“java ConnectTester www.javathinker.net 80”,ConnectTester类的connect()方法会请求连接Internet网上www.javathinker.net主机的80端口,如果连接成功,就会打印如下结果:
在这里插入图片描述
以上打印结果表明客户与服务器建立连接花了49ms(毫秒),在不同的主机上运行该程序,会有不同的打印结果。

1.5、使用代理服务器

在实际应用中,有的客户程序会通过代理服务器来访问远程服务器。代理服务器有许多功能,比如能作为防火墙进行安全防范,或者提高访问速度,或者具有访问特定远程服务器的权限。

以下程序代码通过代理服务器来连接远程的www.javathinker.net服务器:
在这里插入图片描述
Proxy.Type类表示代理服务器的类型,有以下可选值:

  • Proxy.Type.SOCKS:在分层的网络结构中,Type.SOCKS是位于会话层的代理类型。
  • Proxy.Type.HTTP:在分层的网络结构中,Type.HTTP是位于应用层的代理类型。
  • Proxy.Type.DIRECT:不使用代理,直接连接远程服务器。

1.6、InetAddress地址类的用法

InetAddress类表示主机的IP地址,InetAddress类的静态工厂方法getByName()用于构造自身的实例,例如:
在这里插入图片描述
InetAddress的getByName()方法的参数可以是IP地址或主机名。一般说来,主机名比IP地址要固定许多,主机名通常不会发生变化,而IP地址可能会发生变动。所以,在通过Socket连接某个服务器时,应该优先考虑提供主机名。

以下程序代码打印www.javathinker.net的地址信息:
在这里插入图片描述

InetAddress还提供了获取相应的主机名的两种方法:

  • getHostname():首先从DNS缓存中查找与IP地址匹配的主机名,如果不存在,再通过DNS服务器查找,如果找到,则返回主机名,否则返回IP地址。
  • getCanonicalHostName():通过DNS服务器查找与IP地址匹配的主机名,如果找到,则返回主机名,否则返回IP地址。

以上两种方法的区别在于,getHostname()会先查找DNS缓存,减少查找DNS服务器的概率,这样做能提高查找性能。因为查找DNS服务器是很耗时的操作。而getCanonicalHostName()总是查找DNS服务器,确保获得当前最新版本的主机名。

以下程序代码打印本地主机的主机名:
在这里插入图片描述
InetAddress类还提供了两个测试能否从本地主机连接到特定主机的方法:

public boolean isReachable(int timeout)throws IOException;
public boolean isReachable(NetworkInterface interface,int ttl,int timeout)throws IOException;

如果远程主机在参数timout(以ms为单位)指定的时间内做出回应,以上方法返回true,否则返回false。如果出现网络错误则抛出IOException。第2种方法还允许从参数指定的本地网络接口建立连接,以及TTL生存时间。TTL( Time To Live)指IP数据包被丢弃前允许存在的时间。

以下程序代码测试本地主机是否能在10s内连接www.javathinker.net网站:
在这里插入图片描述
下面介绍一个运用InetAddress类的实用范例。很多服务器会监视垃圾邮件发送者(spammer)。Spamhaus是一家国际知名反垃圾邮件机构,作为一个国际性非营利组织,提供了这一项服务。它的官方网站收集了常见的垃圾邮件发送者的IP地址的列表。例如,如何判断IP地址“108.33.56.27”是否是垃圾邮件发送者的地址呢?其步骤为调用InetAddress的getByName()方法,该方法通过DNS服务器查找“27.56.33.108.sbl.spamhaus.org”,如果查找成功(更确切地说,是返回IP地址为“127.0.0.2”的InetAddress对象),就说明这是一个垃圾邮件发送者的地址。否则,getByName()方法抛出UnknownHostException异常,就说明这不是一个垃圾邮件发送者的地址。

下面的SpamCheck类演示了如何判断特定IP地址是否为垃圾邮件发送者的IP地址。
在这里插入图片描述
通过命令“java SpamCheck 108.33.56.27 45.78.123.5154.65.93.21”运行SpamCheck类,会得到以下打印结果。
在这里插入图片描述

1.7、NetworkInterface类的用法

NetworkInterface类表示物理上的网络接口,它有两种构造自身实例的静态工厂方法,这两种方法都声明抛出SocketException。

  • getByName(String name):参数name是指定网络接口的名字。如果不存在与名字对应的网络接口,就返回null。
  • getByInetAddress(InetAddress address):参数address是指定网络接口的IP地址。如果不存在与IP地址对应的网络接口,就返回null。

以下程序代码演示如何创建NetworkInterface对象:
在这里插入图片描述
NetworkInterface类的以下方法用于获取网络接口的信息:

  • public String getName():返回网络接口的名字。
  • public Enumeration getInetAddresses():返回和网络接口绑定的所有IP地址。返回值为Enumeration类型,里面存放了表示IP地址的InetAddress对象。

2、获取Socket的信息

在一个Socket对象中,同时包含了远程服务器的IP地址和端口信息,以及客户本地的IP地址和端口信息。此外,从Socket对象中还可以获得输出流和输入流,分别用于向服务器发送数据,以及接收从服务器端发来的数据。以下方法用于获取Socket的有关信息:

  • getInetAddress():获得远程被连接进程的IP地址。
  • getPort():获得远程被连接进程的端口。
  • getLocalAddress():获得本地的IP地址。
  • getLocalPort():获得本地的端口。
  • getInputStream():获得输入流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownInput()方法关闭输入流,那么此方法会抛出IOException。
  • getOutputStream():获得输出流。如果Socket还没有连接,或者已经关闭,或者已经通过shutdownOutput()方法关闭输出流,那么此方法会抛出IOException。

下面的HTTPClient类用于访问网页www.javathinker.net/index.jsp。该网页位于一个主机名(也叫域名)为“www.javathinker.net”的远程HTTP服务器上,它监听80端口。在HTTPClient类中,先创建了一个连接到该HTTP服务器的Socket对象,然后发送符合HTTP的请求,接着接收从HTTP服务器上发回的响应结果。
在这里插入图片描述
以上HTTPClient类在发送数据时,先把字符串形式的请求信息转换为字节数组(即字符串的编码),然后发送。
在这里插入图片描述
HTTPClient类在接收数据时,把接收到的字节写到一个ByteArrayOutputStream中,它具有一个容量能够自动增长的缓冲区。如果socketIn.read(buff)方法返回“-1”,则表示读到了输入流的末尾。
在这里插入图片描述
当运行HTTPClient程序时,会打印服务器端发送的HTTP响应结果。
在这里插入图片描述
HTTP响应结果包括响应头和响应正文,中间以空行隔开。以上响应正文部分是乱码。这是因为www.javathinker.net服务器在发送正文内容时,先把它压缩成为GZIP格式,客户端需要对压缩数据进行解压,才能得到正文内容。而本范例未对压缩的正文数据解压,就直接将它打印出来,所以会显示乱码。

3、关闭Socket

当客户与服务器的通信结束时,应该及时关闭Socket,以释放Socket占用的包括端口在内的各种资源。Socket的close()方法负责关闭Socket。如果一个Socket对象被关闭,就不能再通过它的输入流和输出流进行I/O操作,否则会导致IOException。

为了确保关闭Socket的操作总是被执行,可以把这个操作放在finally代码块中:
在这里插入图片描述
Socket类提供了3个状态测试方法:

  • isClosed():如果Socket没有关闭,则返回false,否则返回true。
  • isConnected():如果Socket曾经连接到远程主机,不管当前是否已经关闭,都返回true。如果Socket从未连接到远程主机,就返回false。
  • isBound():如果Socket已经与一个本地端口绑定,则返回true,否则返回false。

如果要判断一个Socket对象当前是否处于连接状态,可采用以下方式:
在这里插入图片描述

以下这段代码演示了isClosed()和isConnected()方法在各种场景中的取值:
在这里插入图片描述
提示:Socket和ServerSocket,以及ServerSocketChannel、SocketChannel、SSLServerSocket和SSLSocket等若都实现了java.lang.Auto Closable接口。这意味着如果在try代码块中打开或创建了这些类的实例,那么即使程序没有显式地关闭它们,Java虚拟机也会在退出try代码块时自动关闭它们,释放相关的资源。另一方面,尽管这些类具有自动关闭的功能,仍然建议在程序中及时显式地关闭它们,这样可以提高程序的健壮性并提高其性能。

4、半关闭Socket

进程A与进程B通过Socket通信,假定进程A输出数据,进程B读入数据。进程A如何告诉进程B所有数据已经输出完毕呢?有几种处理办法。

(1)如果进程A与进程B交换的是字符流,并且都一行一行地读写数据,那么可以事先约定以一个特殊的标志作为结束标志,例如以字符串“bye”作为结束标志。当进程A向进程B发送一行字符串“bye”,进程B读到这一行数据后,就停止读取数据。
在这里插入图片描述
(2)进程A先发送一个消息,告诉进程B所发送的正文的长度,然后发送正文。进程B先获知进程A将发送的正文的长度,接下来只要读取该长度的字符或者字节,就停止读取数据。

(3)进程A发完所有数据后,关闭Socket。当进程B读入了进程A发送的所有数据后,再次执行输入流的read()方法时,该方法返回“-1”,如果执行BufferedReader的readLine()方法,那么该方法返回null:
在这里插入图片描述
(4)当调用Socket的close()方法关闭Socket后,它的输出流和输入流也都被关闭。有的时候,可能仅仅希望关闭输出流或输入流之一。此时可以采用Socket类提供的半关闭方法:

  • shutdownInput():关闭输入流。
  • shutdownOutput():关闭输出流。

假定进程A执行以下代码,先向进程B发送一个字符串,等到进程B接收到这个字符串后,进程A再调用Socket的shutdownOutput()方法关闭输出流。接下来进程A不允许再输出数据,但是仍可以通过输入流读入数据:
在这里插入图片描述
进程B在读入数据时,如果进程A的输出流已经关闭,进程B读入所有数据后,就会读到输入流的末尾。

值得注意的是,先后调用Socket的shutdownInput()和shutdownOutput()方法,仅仅关闭了输入流和输出流,并不等价于调用Socket的close()方法。在通信结束后,仍然要调用Socket的close()方法,因为只有该方法才会释放Socket占用的资源,比如占用的本地端口等。

Socket类还提供了两种状态测试方法,用来判断输入流和输出流是否关闭:

  • public boolean isInputShutdown():如果输入流关闭,则返回true,否则返回false。
  • public boolean isOutputShutdown():如果输出流关闭,则返回true,否则返回false。

当客户与服务器通信时,如果有一方突然结束程序,或者关闭了Socket,或者单独关闭了输入流或输出流,对另一方会造成什么影响呢?以下就用Sender类和Receiver类来演示。Sender表示发送数据的客户程序,它每隔500ms发送一行字符串,共发送20行字符串。Receiver表示接收数据的服务器程序,它每隔1s接收一行字符串,共接收20行字符串。
在这里插入图片描述

在这里插入图片描述
Sender类和Receiver类的stopWay成员变量用来指定结束通信的方式。stopWay变量的默认值为1,表示自然结束通信,此外,用户可以通过命令行参数来设置stopWay变量的值。

1.自然结束Sender和Receiver的通信

先运行“java Receiver”,再运行“java Sender”,Sender会发送20行字符串,然后自然结束运行,Receiver会接收20行字符串,然后也自然结束运行。

2.提前终止Receiver

先运行“java Receiver 2”,或者“java Receiver 3”,或者“java Receiver 4”,或者“java Receiver 5”,然后运行“java Sender”。Receiver接收了3行字符串后,就结束运行。但是Sender仍然会发送完20行字符串后,才自然结束运行。之所以会出现这种情况,是因为尽管Receiver已经结束运行,但底层的Socket并没有立即释放本地端口,操作系统探测到还有发送给该Socket的数据,会使底层Socket继续占用本地端口一段时间。

3.突然终止Sender

先运行“java Receiver”,再运行“java Sender 2”,Sender发送了3行字符串后,在没有关闭Socket的情况下,就结束运行。Receiver在第4次执行BufferedReader的readLine()方法时会抛出异常。
在这里插入图片描述

4.关闭或者半关闭Sender的Socket

先运行“java Receiver”,再运行“java Sender 3”,或者运行“java Sender 4”。Sender发送了3行字符串后,会关闭Socket(运行“java Sender 3”),或者关闭Socket的输出流(运行“java Sender 4”),然后结束运行。Receiver在第4次执行BufferedReader的readLine()方法时读到输入流的末尾,因此readLine()方法返回null。

5、设置Socket的选项

Socket的选项如下所示:

  • TCP_NODELAY:表示立即发送数据。
  • SO_RESUSEADDR:表示是否允许重用Socket所绑定的本地地址。
  • SO_TIMEOUT:表示接收数据时的等待超时时间。
  • SO_LINGER:表示当执行Socket的close()方法时,是否立即关闭底层的Socket。
  • SO_SNDBUF:表示发送数据的缓冲区的大小。
  • SO_RCVBUF:表示接收数据的缓冲区的大小。
  • SO_KEEPALIVE:表示对于长时间处于空闲状态的Socket,是否要自动把它关闭。
  • OOBINLINE:表示是否支持发送1字节的TCP紧急数据。
  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java网络编程(第三版)中文版(不看后悔) JAVA Network Programming,Third Edition 原出版社: O'Reilly 作者: (美)Elliotte Rusty Harold [作译者介绍] 译者: 朱涛江[同译者作品] 林剑 丛书名: O'Reilly Java系列 出版社:中国电力出版社 编辑推荐 “直到找到这本书之后,我才开始理解Java网络编程。” ——Bruce Eckel,《Thinking in Java》的作者 内容简介回到顶部↑《Java网络编程》第三版会为你介绍Java网络API的最新特性。本书讨论了JDK 1.4和1.5(现在已命名为J2SE 5)中所做的所有修改和增补。本书内容全面,涵盖了从网络基础知识到远程方法调用(RMI)等各方面的内容,书中章节涉及到TCP和UDP socket、服务器socket、URL和URI、组播以及特殊用途的API(如JavaMail)等等。本书展示了如何使用JSSE编写安全的网络应用程序,解释了如何使用NIO API编写超高性能的服务器。它还涵盖了Java网络代理、Web cookie和URL缓存的支持。 《Java网络编程》不仅仅是对API的解释:它还展示了如何使用API。本书有很多示例,包含了几千行可以实际工作的代码(所有代码都可以在线获得),实现了功能完整的网络客户端和服务器。无论是希望编写特殊用途的web服务器、安全的在线订单接收程序、简单的组播代理还是电子邮件客户端,都会找到可供学习和借用的代码。 本书适合熟悉Java语言的读者的编程人员和计算机专业的学生阅读。 前言 1 第一章 Java网络编程的原因 13 网络程序的功能 14 安全性 27 等等!还有更多! 29 第二章 基本网络概念 30 网络 30 网络的分层 32 IP、TCP和UDP 37 Internet 40 客户/服务器模型 46 Internet标准 47 第三章 基本Web概念 56 URI 56 HTML、SGML和XML 63 HTTP 65 MIME媒体类型 69 服务器端程序 74 第四章 流 78 输出流 79 .输入流 83 过滤器流 87 阅读器和书写器 101 第五章 线程 116 运行线程 118 返回线程中的信息 122 同步 133 死锁 139 线程调度 140 线程池 153 第六章 查找Internet地址 159 InetAddress类 161 Inet4Address和Inet6Address 177 NetworkInterface类 178 一些有用的程序 181 第七章 URL和URI 192 URL类 192 URLEncoder和URLDecoder类 216 URI类 222 代理 230 通过GET方法与服务器端程序通信 233 访问受口令保护的网站 237 第八章 Swing中的HTML 245 组件上的HTML 245 JEditorPane 247 解析HTML 256 cookie 274 第九章 客户端Socket 283 socket基础 283 用Telnet研究协议 284 Socket类 286 Socket异常 312 Socket地址 313 示例 314 第十章 服务器socket 332 ServerSocket类 332 一些有用的服务器 349 第十一章 安全Socket 370 保护通信 371 创建安全客户端Socket 374 SSLSocket类的方法 378 创建安全的服务器Socket 383 SSLServerSocket类的方法 388 第十二章 非阻塞I/O 391 一个示例客户端 392 一个示例服务器 396 缓冲区 402 通道 421 就绪选择 427 第十三章 UDP数据报和Socket 431 UDP协议 431 DatagramPacket类 433 DatagramSocket类 442 一些有用的应用程序 456 DatagramChannel 469 第十四章 组播socket 478 何为组播socket? 479 使用组播socket 487 两个简单示例 495 第十五章 URLConnection 501 打开URLConnection 502 读取服务器的数据 503 读取首部 505 配置连接 514 配置客户端的请求HTTP首部 523 向服务器写入数据 525 内容处理器 530 Object方法 532 URLConnection的安全考虑 533 猜测MIME内容类型 533 HttpURLConnection 537 缓存 552 JarURLConnection 557 第十六章 协议处理器 560 何为协议处理器? 560 URLStreamHandler类 564 编写协议处理器 571 更多协议处理器示例和技术 576 URLStreamHandlerFactory接口 583 第十七章 内容处理器 588 何为内容处理器? 590 ContentHandler类 592 ContentHandlerFactory接口 603 FITS图片格式的内容处理器 606 第十八章 远程方法调用 617 何为远程方法调用? 617 实现 623 在运行时加载类 631 java.rmi包 634 java.rmi.registry包 640 java.rmi.server包 642 第十九章 JavaMail API 648 何为JavaMail API? 649 发送电子邮件 651 接收邮件 661 口令认证 666 地址 670 URLName类 674 Message类 677 Part接口 689 多部分消息和附件 699 MIME消息 703 文件夹 705

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值