黑马程序员——网络编程2:网络通讯组件介绍及演示-上

来源:互联网 发布:英雄三国辅助软件 编辑:程序博客网 时间:2024/05/29 04:48
------Java培训、Android培训、iOS培训、.Net培训、期待与您交流! -------

网络编程2:网络通讯组件介绍及演示-上

        通过前面的内容,我们简单了解了实现网络通讯所必须的前提条件,以及网络传输的实现原理。下面我们就通过代码的形式来实现网络通讯。

        按照面向对象的思想,Java语言中将一些网络通讯所需的组件都封装为了各个类,我们只需要创建或者获取这些类的实例对象并通过调用它们的方法,即可实现网络通讯。用于网络编程所需的所有类均包含在Java标准类库java.net工具包中。

1  IP地址及其主机名——InetAddress

        当我们登录大型门户网站时,实际上我们访问的是保存有该网站网页文件的服务器。而服务器作为计算机,同样具有IP地址,但通常我们在浏览器中输入的并非是IP地址,而是网站的主机名。因为相对于4个数字,由英文字母组成的主机名更方便记忆。比如,我们前面曾经提到的本地回环地址127.0.0.1,其对应的主机名默认为localhost;再比如,“百度”的主机名是“www.baidu.com”,新浪的主机名为“www.sina.com”等等。那么Java语言中,用于描述IP地址及其对应主机名之间映射关系的类称为InetAddress。

1.1  InetAddress类简介

API文档:

        InetAddress类表示互联网协议(IP)地址。IP地址是使用的32位或128位的无符号数字,它是一种低级协议,UDP和TCP协议都是在它的基础上构建的。

        该类有两个子类,分别是表示IPv4的Inet4Addresss以及表示IPv6的Inet6Address。

方法:

        构造方法:该类不具备构造方法,因此不能通过“new”关键字创建出来。那么按照以往的经验,该类可能会提供用以获得本类对象的静态方法。

        方法:

                publicstatic InetAddress getLocalHost() throws UnknownHostException:返回本地主机。如果无法确定主机的IP地址将抛出UnknownHostException,该异常属于IOException的子类,因此调用该方法前,需要导入java.io包。

                publicstatic InetAddress getByName(String host) throws UnknownHostException:在给定主机名的情况下确定主机的IP地址。主机名可以是机器名(如“java.sun.con”),也可以是其IP地址的文本表示形式。如果提供面值IP地址,则仅检查地址格式的有效性。该方法同样可能抛出UnknownHostException。

                publicstatic InetAddress[] getAllByName(String host) throws UnknownHostException:在给定主机名的情况下,根据系统上配置的名称服务返回其IP地址所组成的数组。像“百度”这样的大型网站,由于每天的访问量非常巨大,因此通常有若干服务器同时运转,而每个服务器的IP地址都是不同的。因此该方法的作用就是可以获取到指定主机名对应的所有服务器IP地址对象,并将其封装为一个InetAddress数组返回。

                publicstatic InetAddress getByAddress(byte[] addr) throws UnknownHostException:在给定原始IP地址的情况下,返回InetAddress对象。该方法的作用是返回指定IP地址对应的InetAddress对象,只不过传递的参数类型为IP地址的字节数组形式,例如IP地址“192.169.1.202”的字节数组形式为:

        byte[] ip = {192, 169, 1, 202};

        由于参数较为复杂,因此该方法的使用频率较低。

1.2  InerAddress类应用


需求1:获取本机IP地址和主机名,并将其打印在控制台。

代码1:

import java.net.*;import java.io.*; class IPDemo{public static void main(String[] args) throws IOException{//创建封装有本机IP地址和主机名的InetAddress对象InetAddress localHost = InetAddress.getLocalHost();//打印本机IP地址和主机名System.out.println(localHost);             //分别获取本机IP地址和主机名,并分别打印String hostName = localHost.getHostName();String hostAddress = localHost.getHostAddress();System.out.println("hostName= " + hostName);System.out.println("hostAddress= " + hostAddress); }}
以上代码的执行结果为:

Akx-PC/169.254.100.231

hostName= Akx-PC

hostAddress= 169.254.100.231

直接打印InetAddress对象,将同时打印本机主机名和IP地址。也可以通过getHostName()和getHostAddress()方法分别获取到本机主机名和IP地址。

 

需求2:获取指定主机名的IP地址。

代码2:

import java.net.*;import java.io.*; class IPDemo2{public static void main(String[] args) throws IOException{//获取封装有指定IP地址对应的主机名和IP地址的InetAddress对象//指定IP地址InetAddress ip = InetAddress.getByName("169.254.100.232");System.out.println(ip);             //分别获取本机IP地址和主机名,并分别打印String hostName = ip.getHostName();String hostAddress = ip.getHostAddress();System.out.println("hostName= " + hostName);System.out.println("hostAddress= " + hostAddress);System.out.println(); //获取封装有“百度”的IP地址和主机名的InetAddress对象//指定主机名InetAddress baidu = InetAddress.getByName("www.baidu.com");System.out.println(baidu);             //分别获取“百度”的主机IP地址和主机名,并分别打印String baidu_name = baidu.getHostName();String baidu_address = baidu.getHostAddress();System.out.println("hostName= " + baidu_name);System.out.println("hostAddress= " + baidu_address);}}
执行以上代码的结果为:

/169.254.100.232

hostName= 169.254.100.232

hostAddress= 169.254.100.232

 

www.baidu.com/119.75.218.70

hostName= www.baidu.com

hostAddress= 119.75.218.70

        以上代码中分别使用了两种方式获取InetAddress对象,一种是通过指定IP地址,另一种是通过指定主机名。

        通过第一种方式虽然成功获取了任意指定的IP地址对应的InetAddress对象,但却无法打印具体的主机名,并且调用getHostName的返回值仍旧是IP地址。这是因为,当指定IP地址和对应主机名的映射关系并没有存在于网络中时,InetAddress对象将无法解析该IP地址,结果就是无法获取主机名。由于本机主机名和IP地址的映射关系存储于本机中,因此能够进行解析。具体的解析原理将在后面的内容中介绍。

        通过第二部分代码成功获取并打印了“百度”对应的主机名和IP地址,由于“百度”主机名和IP地址的映射关系存在于网络中,因此能够解析。大家可以自行测试其他常用网站,比如“新浪”、“搜狐”等等。

 

需求3:通过getAllByName(Stringhost)方法,获取并打印指定主机名对应的所有IP地址和主机名。

代码3:

impor tjava.net.*;import java.io.*; class IPDemo3{public static void main(String[] args) throws IOException{//获取并打印“百度”所有服务器的IP地址对象InetAddress[] baidus = InetAddress.getAllByName("www.baidu.com"); for(InetAddress baidu : baidus){System.out.println("hostName= "+baidu.getHostName());System.out.println("hostAddress= "+baidu.getHostAddress());}}}
以上代码的执行结果为:

hostName= www.baidu.com

hostAddress= 119.75.217.109

hostName= www.baidu.com

hostAddress= 119.75.218.70

        显然,“百度”网站使用了两个服务器,以应对较大的访问量。大家可以自行测试其他常用网站。

2  套接字——Socket

2.1  Socket简介

        Socket的本意是插座的意思,而在网络编程中我们翻译为套接字。我们指的网络编程其实就是指的Socket编程,因此网络编程就是围绕着Socket来展开的。

每一个网络软件内部都会有一个Socket,它的作用就是在具备了网络通讯三要素的前提下,通过某种网络参考模型,与对方Socket实现网络通讯的一个组件,它其实也是一个程序。Socket主要负责的就是通过网络接收对方发送的数据,经所属软件的其他组件处理以后,再将处理数据再发送给对方,当然数据的接收和发送都要经过物理网络传输介质(网卡、网线等)。

        我们可以将Socket形象的比喻为一个码头。如果两地之间要通过海路实现通商,那么首先就要有码头,而穿上的货物就是数据。码头的作用就接收另一个地方运送过来的货物,并送往码头所在城市的各个地方。

        说了这么多,我们对Socket的特点及作用做一个简单的总结:

        (a)  Socket就是为网络服务提供的一种机制;

        (b)  网络通讯的两端都必须有Socket;

        (c)  网络通讯其实就是Socket之间的通讯;

        (d)  数据在两个Socket之间通过IO进行传输。我们在前面介绍IO技术的内容,曾经实现过对各种格式文件的复制过程。那么其实当从一个Socket中传输一个图片到另一个Socket,实际也是一个复制的过程,只不过复制过程并不是发生在一个硬盘的两个位置,而是通过网络发生在两台计算机而已。

        Java针对不同的传输协议提供了不同的Socket类——代表UDP协议的DatagramSocket,以及代表TCP协议的ServerSocket。下面我们将详细介绍这两个类的特点及应用。

2.2  UDP传输协议套接字——DatagramSocket& DatagramPacket

(1)  DatagramSocket

API文档:

        此类表示用来发送和接收数据报包的套接字。

方法:

        构造方法:

                pubicDatagramSocket() throws SocketException:构造数据报套接字,并将其绑定到本机主机上任何可用的端口。套接字将被绑定到通配符地址,IP地址由内核来选择。如果套接字不能被打开,或不能将其绑定到指定的本机端口时,将会抛出SocketException。

                publicDatagramSocket(int port) throws SocketException:创建数据报套接字并将其绑定至本地主机上的指定端口。套接字将被绑定包通配符地址,IP地址有内核来决定。通常此构造方法用于Udp传输服务的接收端。

        方法:

                publicvoid send(DatagramPacket p) throws IOException:从此套接字发送数据报包。该方法的参数类型为DatagramPacket,而我们在前面的内容中提高,UDP协议的特点之一就是将发送的数据封装为一个一个的数据包,而DatagramPacket就是用于描述数据包的类,该类实例对象中将包含有:将要发送的数据、数据的长度、对方主机的IP地址和端口号等信息。关于DatagramPacket类的详细介绍请参考本节第二部分内容——(2)  DatagramPacket。

                由于在创建数据报包对象时可能会指定错误的IP地址或者端口号,造成传输失败,因此该方法声明抛出IOException异常。

                publicvoid receive(DatagramPacket p) throws IOException:从此套接字接收数据报包。当此方法返回时,DatagramPacket的缓冲区填充了接收的数据。数据报包也包含发送方的IP地址和发送方计算机的端口号。注意到该方法的参数类型也是DatagramPacket,也就是所调用该方法时传递的DatagramPacket对象,用于接收对方发送的数据报包中的内容,相当于我们之前介绍的缓冲读写流中的字符/字节缓冲的作用。然后通过数据报包对象的各种方法,获取到其中的数据,以及发送方IP地址、端口等信息。

(2)  DatagramPacket

API文档:

        此类表示数据报包。数据报包用来实现无连接包投递服务。每条报文仅根据该包中包含的信息从一台计算机路由到另一台计算机。DatagramPacket类实例对象的首要功能就是用于封装将要传输的数据。其次用于封装目的计算机的IP地址、端口号等信息,这可以从下面的构造方法中看出。

方法:

        构造方法:该类的构造方法分为两种,一种是用于封装将要被发送的数据,另一种是用于封装接收的数据。那么这两种构造方法的区别在于,前者需要指定IP地址和端口号,后者则不需要。

        publicDatagramPacket(byte[] buf, int length, InetAddress address, int port):构造数据报包,用来将长度为length的包发送到指定主机的指定端口号。length参数必须小于等于buf.length。该方法由于需要指定IP地址和端口号,因此用于发送数据。字节数组buf内存储的就是将要传输的字节数据,address就是封装与目的计算机IP地址和主机名的InetAddress对象,port表示的就是端口号。

        publicDatagramPacket(byte[] buf, int length):构造DatagramPacket,用来接收长度为length的数据包。length参数必须小于等于buf.length。该方法即是用于接收数据的构造方法。

(3)  UDP传输服务演示实例

        简要介绍了DatagramSocket类以及DatagramPacket类特点和方法以后,我们通过下面的例子对他们进行一些简单的应用。

 

需求1:通过UDP传输协议,将一段文字数据发送出去。

思路:

        (1)  创建DatagramSocket对象,建立UDP传输服务,以此作为收发数据的端点;

        (2)  将被传输数据、接收方IP地址及端口信息封装到DatagramPacket对象中。

        (3)  通过DatagramSocket对象的send方法,将以上数据报包发送出去。

        (4)  由于进行网络通讯都是需要消耗系统的底层资源的,因此数据传输完毕以后,需要关闭资源。

代码4:

import java.net.*;import java.io.*; //发送端classUdpSend{public static void main(String[] args) throws IOException{//1.  通过创建DatagramSocket对象,建立Udp数据传输服务。DatagramSocket ds = newDatagramSocket(); //2.  创建数据包。byte[] buf = "HelloWorld!".getBytes();//字节数组形式的传输数据int length = buf.length;//数据长度,也即字节数组长度InetAddress targetIp = InetAddress.getLocalHost();//创建接收端IP地址对象int port  = 10000;//端口号DatagramPacket dp = newDatagramPacket(buf, length, targetIp, port);//根据以上信息,创建数据包 //3.  通过Udp数据传输服务,使用send方法将以上数据包发送出去ds.send(dp); //4.  关闭资源ds.close();}}
执行以上代码将不会有任何现象产生。

代码说明:

        (1)  为了演示方便,数据报包对象中指定的接收方IP地址为本机IP地址。也就是说,数据的发送方和接收方均为同一台计算机。

        (2)  以上代码中,有若干代码可能会抛出异常,但这里仅为演示Udp传输服务的建立,数据报包的创建以及数据的发送,因此暂时不进行异常处理。

        (3)  代码1实际上仅仅创建了Udp数据传输服务的发送端,而并没有打开数据接收端,因此按照Udp协议面向无连接的特点,数据能够发送出去,但是由于没有接收端而丢失。因此下面我们还要定义接收端代码。

 

需求2:创建Udp传输服务的接收端。

思路:无论是发送端还是接受端,其实都是一个能够独立运行的程序,因此双方代码中都应具备主函数。

        (1) 通过创建DatagramSocket对象,建立Udp传输服务,并为其指定一个端口;

        (2) 创建一个数据报包对象,用于接收对方发送的数据,以及发送方IP地址、端口等信息;

        (3) 通过DatagramSocket对象的receive()方法,将接收到的DatagramPacket对象中的数据和信息存入自定义数据报包对象内;

        (4) 通过数据报包对象的特有方法,将其中的数据取出,并打印在控制台中。

        (5) 关闭资源。

代码5:

import java.net.*;import java.io.*; //接收端class UdpReceive{public static void main(String[] args) throws IOException{//1.  建立Udp传输服务。DatagramSocket ds = newDatagramSocket(); //2.  定义数据报包,用于存储接收的数据和信息。//这里应使用用于接收数据的DatagramPacket构造方法 。byte[] buf = new byte[1024];int length = buf.length;DatagramPacket dp = newDatagramPacket(buf, length); //3.  通过DatagramSocket对象的receive()方法,将接收到的数据存入自定义DatagramPacket对象中。ds.receive(dp); //4.  通过数据报包对象的特有方法获取其中的数据。String ip = dp.getAddress().getHostAddress();//获取到数据发送方的IP地址String data = newString(dp.getData(), 0, dp.getLength());//将接收到的数据中的有效部分封装为一个字符串int port = dp.getPort();             //将数据和信息打印到控制台System.out.println("ip = "+ip+"\nport= "+port+"\ncontent:"+data); //关闭资源ds.close();}}
        此时,发送端和接收端均已定义完毕,可以进行数据的收发测试。执行时开启两个命令行,首先在其中一个命令行中编译并执行接收端,然后在另一个命令行中编译并执行发送端,就可以在接收端中接收到数据个信息。当然也可以先执行发送端,但由于此时并未开启接收端,因此数据将会丢失。

接收端的显示结果为:

        ip =192.168.1.100

        port =60927

        content:HelloWorld!

其中60927是发送端端口号。由于并没为发送端指定任何端口,因此该端口号是系统指定端口号。

代码说明:

        (1)  需要强调一点,在通过发送端发送数据时,我们在数据报包对象中定义了接收端的端口号,因此在建立接收端Udp传输服务时,也一定要为其定义相同的端口号。这就需要调用DatagramSocket带有(int port)参数的构造方法为其指定端口号。相反如果通过空参数构造方法创建对象,系统将会自定为其分配端口。

        (2)  注意到,开启接收端以后,程序将会停止执行并等待,这是因为DatagramSocket类的receive()方法是阻塞式方法,若没有接收到任何数据就会处于等待状态。其实底层利用的就是线程的等待唤醒机制,开启接收端线程后,若没有接收到数据就调用wait()方法,进入冻结状态;接收到数据后调用notify()方法唤醒线程。

        (3)  多次执行以上代码就能够发现,发送端每次使用的端口号都是不同的,那么之所以每次都要更短端口,是因为上一次使用的端口还没有在这么短的时间内被释放,因此就会分配新的端口。当然,我们也可以自己指定发送端端口号。

 

需求3:发送端通过键盘录入信息,并循环发送给接收端;相应的,接收端也循环接收发送端发送的信息。

思路:

        创建接收端与发送端Udp服务的代码与前述代码基本一致。所不同的是:

        发送端:添加键盘录入代码,将键盘录入的文字转换为字节数组,并把数据发送代码定义在循环体内,向接收端循环发送信息;

        接收端:开启while(true)循环,将数据接收代码也定义在循环体内,不断接收数据。

代码6:

import java.net.*;import java.io.*; class UdpSend{public static void main(String[] args) throws IOException{DatagramSocket ds = newDatagramSocket();             //开启键盘录入BufferedReader bufr =new BufferedReader(newInputStreamReader(System.in));             String line = null;byte[] buf = null;InetAddress targetIp = InetAddress.getLocalHost();DatagramPacket dp = null;while((line = bufr.readLine()) !=null){    //当键入“over”时,跳出循环,并结束发送端Udp传输服务。if("over".equals(line))break; buf = line.getBytes();dp = newDatagramPacket(buf, buf.length, targetIp, 10000); ds.send(dp);} ds.close();}}class UdpReceive{public static void main(String[] args)throws IOException{DatagramSocket ds = newDatagramSocket(10000);             byte[] buf = null; while(true){buf = new byte[1024];DatagramPacket dp = newDatagramPacket(buf, buf.length); ds.receive(dp); String data = newString(dp.getData(), 0, dp.getLength());String ip =dp.getAddress().getHostAddress();             System.out.println(ip+": "+data);} //不必关闭接收端Udp服务}}
代码说明:

        (1)  由于receive()方法是阻塞式方法,当没有接收到任何数据的时候就会处于冻结状态,因此即使是while(true)循环,也不会造成无限循环。

        (2) 创建DatagramSocket对象的代码不能定义在while循环体中。否则接收端会报出以下异常提示信息:

                Exception in thread "main" java.net.BindException: Addressalready in use: Cannot bind

        说明指定端口已经有程序在使用了,不能将其绑定到此程序。之所以发生这个异常是因为,如果将创建DatagramSocket对象的代码定义在循环中,那么每执行一次循环,            就会创建一个DatagramSocket对象,而每个对象都会指定同一个端口。那么当创建第二个对象时,就会与前一对象的端口发生冲突而发生异常。

        (3) 如果用于接收数据的字节缓冲区不够大的话,可以将其大小定义为64 * 1024,也就是64kb,这是Udp传输方式一次能够传输的最大数据量。

        (4)  代码6中并没有为接收端Udp服务定义关闭服务代码。这是因为接收端的作用就是用于接收数据,如果因为某个发送端发送了“over”信息后,接收端关闭了服务,那么就无法接收其他发送端发送的数据了。

 

小知识点1:

        代码6实际上已经基本实现了点对点的聊天功能。那么我们能不能在此基础上实现群聊呢?IP地址的末尾名义上可以取0-255中的任意数字,但是有两个数字一般是不使用的,那就是0和255。当IP地址末尾为0时,它代表某一个IP地址段,因此不能作为一个实际的IP地址使用;当末尾地址为255时,它表示广播地址。所谓广播地址,就是可以向该地址段中的所有计算机发送数据。那么当Udp传输服务的发送端IP地址定义为广播地址时,就可以实现群聊了。大家可以自行尝试。

 

需求4:通过Udp传输服务实现一个简单的聊天工具。

思路:

        代码6中,发送端和接收端分别使用了两个命令行运行,那么实际上对应了两个进程。然而一般的聊天软件在运行时,无论发送数据还是接受数据都是在一个进程中运行,因此需要为这两端分配两个线程,以实现同时接受和发送数据的功能。这里就要用到多线程技术。

        由于接受数据和发送数据是两个同时发生的不同的动作,因此需要分别将这两端封装为两个Runnable接口实现类,并分别定义用于接收数据和发送数据的run()方法。

代码7:

import java.io.*;import java.net.*; //发送端class Send implements Runnable{private DatagramSocket ds; /*为发送端线程初始化指定的DatagramSocket对象以建立Udp传输服务*/Send(DatagramSocket ds){this.ds = ds;} public void run(){BufferedReader bufr;String line;byte[] buf;InetAddress targetIp;DatagramPacket dp; //由于run()方法不能抛出任何异常,因此try-catch处理所有可能抛出异常的代码try{targetIp = InetAddress.getLocalHost();}catch (UnknownHostException e){throw new RuntimeException("无法获取本机IP地址!");} try{    bufr =new BufferedReader(new InputStreamReader(System.in));             while((line = bufr.readLine()) != null){    if("over".equals(line))break; buf = line.getBytes();dp = new DatagramPacket(buf,buf.length, targetIp, 10000); ds.send(dp);}}catch(IOException e){throw new RuntimeException("发送端发生异常!");} ds.close();}}//接收端class Receive implements Runnable{private DatagramSocket ds; Receive(DatagramSocket ds){this.ds = ds;} public void run(){byte[] buf;String data, ip;DatagramPacket dp;             try{while(true){buf = newbyte[1024];dp = newDatagramPacket(buf, buf.length); ds.receive(dp); data = newString(dp.getData(), 0, dp.getLength());ip =dp.getAddress().getHostAddress();                    System.out.println(ip+": "+data);}}catch(IOException e){throw new RuntimeException("接收端接收数据失败!");}}}class MyChat{public static void main(String[] args) throws SocketException{//分别建立接收端和发送端Udp服务DatagramSocket sendSocket = newDatagramSocket();DatagramSocket receiveSocket = newDatagramSocket(10001); //开启两个线程,分别运行Udp传输服务的接收端和发送端new Thread(newSend(sendSocket)).start();new Thread(newReceive(receiveSocket)).start();}}
        执行以上代码以前,同样需要打开两个命令行。首先指定将数据发送到对方法计算机的10000端口,并指定本机接收端端口为10001,然后在其中一个命令行中编译并执行代码;接着,将以上两个端口对调,再编译并执行代码。这样一来,相当于在两个计算机中开启了两个聊天软件,A将信息发送到B的10000端口,而B将信息发送到A的10001端口,实现了一个聊天软件基本的信息沟通功能。

代码说明:

        (1) 由于Runnable接口的的run()方法并没有抛出任何异常,因此实现类在复写该方法时,也不能在run()方法中声明抛出异常,若有方法可能抛出异常,必须对其进行try-catch处理。

        (2) 在执行以上代码时,可能会出现,未来得及发送的信息和接收的信息混在一行的情况出现。当发送信息时,只有在按下回车键时才能发送信息,那么如果此时对方发送信息过来就会自定追加在,未来得及发送的信息后面。根本上就是因为信息发送区域和信息接收区域均在一个对话框的原因。而一般的聊天软件这两个区域是分开显示的,因此我们可以结合GUI技术,创建具有图形化界面的聊天软件。

2.3  TCP传输协议套接字——ServerSocket& Socket

        在通过Udp协议进行网络通讯过程中,通讯双方分别称为发送端和接收端。而在TCP网络通讯中,通讯双方分别称为服务端和客户端,分别对应了两个类——代表服务端的ServerSocket和代表客户端的Socket。

(1)  ServerSocket与Socket数据传输原理简介

        TCP传输中并没有数据报包的概念,而是在客户端与服务端建立了网络连接以后,通过Socket流实现服务端与客户端之间数据的传输,如下图所示。所谓Socket流,其底层原理与一般的IO字节流是一样的,只不过是用于网络数据传输而已。Socket流封装在了客户端Socket对象内,可以通过Socket类提供的方法获取。

        通常,服务端会同时面对多个客户端,那么怎样才能将数据准确发送到指定客户端,而不出现混乱呢?解决办法就是,当服务端与某个客户端建立连接以后,首先会获取到代表该客户端的Socket对象,进而获取到该对象内部的Socket流,那么服务端就可以通过客户端的Socket流实现与客户端的网络通讯,如下图所示。

        那么客户端与服务端建立了连接,且都获取到了该客户端的Socket读写流对象以后,就可以实现两端之间的网络通讯。客户端向服务端发送数据,实际就是向流中写入数据,服务端从流中读取数据;反之,服务端向流中写入数据,客户端从流中读取数据,此即客户端接收数据。


(2)  客户端套接字——Socket

API文档:

        此类实现客户端套接字。套接字是两台机器间通讯的端点。

方法:

        构造方法:

                public Socket():通过系统默认类型的SocketImpl创建未连接套接字。关于这一空参数构造函数的使用方法,我们将在后面的内容中简单介绍。

                public Socket(InetAddress address, int port) throws IOException:创建一个流套接字并将其连接到指定的IP地址的指定端口号。从该构造方法可以看出,在建立客户端TCP传输服务的同时就要将其连接到服务端。因为TCP传输服务的特点就是面向连接的,它依赖于两端之间建立的网络通路,在客户端与服务端之间进行通讯。

                public Socket(String host, int port) throwsUnknownHostException, IOException:此构造方法与上述构造方法功能相似,只不过为其初始化字符串形式的主机IP地址,而不是IP地址对象,这样更为方便。但因为这其中还涉及到主机IP地址解析过程,如果解析错误或者传入错误的主机IP地址,将会造成抛出UnknownHostException异常。

        方法:

                public InputStream getInputStream() throwsIOException:返回此套接字的输入流。此即前述Socket读取流,用于读取服务端发送的数据。

                public OutputStream getOutputStream()throws IOException:返回此套接字的输出流。此即前述Socket写入流,用于向服务端发送数据。

                public void close() throws IOException:关闭此套接字。

                public InetAddress getInetAddress():返回套接字连接的地址。通过该方法可以获取该客户端的IP地址信息。

                public void shutdownOutputStream() throwsIOException:禁用此套接字的输出流。对于TCP套接字,任何以前写入的数据都将被发送,并且后跟TCP的正常连接终止序列。如果在套接字上调用shutdownOutput()后写入套接字输出流,则该流将抛出IOException。该方法的作用相当于在向数据接收方的发送结束标记,从而使对方结束数据的读取。但是调用该方法不会影响Socket读取流的工作,虽然不能发送数据但是可以接收数据。

                public void connect(SocketAddress endpoint)throws IOException:将此套接字连接到服务器。当通过空参数Socket构造方法建立客户端Socket服务时,就可以使用该方法,与目标服务器进行连接。关于该方法参数类型SocketAddress,以及该方法的应用,我们将在后面的内容中对此方法进行介绍。

(3)  服务端套接字——ServerSocket

API文档:

        此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。

方法:

        构造方法:

                public ServerSocket(int port) throwsIOException:创建绑定到特定端口的服务器套接字。端口0在所有空闲端口上创建套接字。传入连接指示(对连接的请求)的最大队列长度被设置为50.如果队列满时收到连接指示,则拒绝该连接。该方法表明,在建立TCP服务端传输服务时,需要为其指定一个端口号,以便客户端明确数据发送目的地。

                public ServeSocket(int port, int backlog)throws IOException:利用指定的backlog创建服务器套接字并将其绑定到指定的本地端口号。参数backlog表示队列的最大长度,代表的是该服务器能够同时连接的客户端数量。一个服务器的处理性能是有限的,因此相应的,同一时间能够连接的客户端数量也是有限的,因此设置这个参数的目的就是限制客户端的连接数量。假设设置这一参数的值为50,那么第51个客户端将无法连接到该服务器,只有前50个客户端中有一个断开连接才能有新客户端连接进来。

        方法:

                public Socket accept() throws IOException:侦听并接受到此套接字的连接。此方法在连接传入之前一直阻塞。调用该方法获取到客户端Socket对象的同时,建立了服务端与该客户端之间的网络连接。

(4)  TCP传输服务演示实例

        下面我们通过ServerSocket和Socket对象,以及它们的各种方法来实现服务端与客户端之间的通讯。在此之前,我们先简单介绍TCP客户端/与服务端的建立,以及实现数据传输的步骤。

        TCP客户端传输服务建立与数据传输步骤:

        第一步:创建Socket对象,并为其初始化将要连接的主机IP地址和端口,以此建立TCP客户端传输服务;

        第二步:为了实现与服务端的网络通讯,获取到Socket读取流和写入流对象;

        第三步:通过写入流的write方法,向Socket流中写入数据;

        第四步:关闭资源。

        通过以上四个步骤,即可建立TCP客户端传输服务,并实现向服务端发送数据的功能。需要注意的是,不必再单独关闭Socket写入流,它会随着Socket客户端服务的关闭而关闭。

        TCP服务端传输服务的建立与数据传输步骤:

        第一步:创建ServerSocket对象,为其初始化一个端口号,建立TCP服务端传输服务;

        第二步:通过调用accept()方法,获取连接到此服务端的客户端Socket对象,建立与该客户端之间的网络连接。

        第三步:通过客户端Socket对象,获取该对象内的Socket读写流对象。

        第四步:通过客户端Socket读取流接收客户端发送的数据,或者通过写入流向客户端发送数据。

需求1:客户端向服务端发送一段文本数据。服务端接收到此数据后,将其打印在控制台。

代码8:

import java.io.*;import java.net.*; //客户端classTcpClient{//无论是客户端还是服务端,都是需要独立运行的程序,因此都需要定义主函数public static void main(String[] args) throws IOException{//1.  建立TCP客户端Socket服务,为其初始化服务端IP地址和端口Socket clientSocket =new Socket(InetAddress.getLocalHost(),10000);//为演示方便,客户端和服务端均定义为本机 //2.  为了向服务端发送数据,获取Socket写入流对象OutputStream out =clientSocket.getOutputStream(); //3.  通过写入流的write方法,向Socket流中写入数据out.write("HelloServer!".getBytes()); //4.  关闭资源clientSocket.close();}}//服务端classTcpServer{public static void main(String[] args) throws IOException{//1.  创建ServerSocket对象,为其初始化一个端口,建立TCP服务端Socket服务ServerSocket serverSocket =new ServerSocket(10000); //2.  获取连接进来的客户端Socket对象,以此建立与客户端之间的连接Socket clientSocket = serverSocket.accept();//获取该客户端的IP地址String clientIP = clientSocket.getInetAddress().getHostAddress();//打印客户端连接成功信息System.out.println(clientIP+"......connected"); //3.  获取客户端Socket读取流,从流中读取客户端发送的数据InputStream in = clientSocket.getInputStream(); //常规的字节读取流读取数据的代码byte[] buf = new byte[1024];int len = in.read(buf); System.out.println(clientIP+": "+new String(buf, 0, len)); //4.  关闭客户端clientSocket.close();}}
执行以上代码以前,同样需要开启连个命令行。通过其中一个命令行首先执行服务端程序,然后再利用另一个命令行执行客户端程序,则服务端所显示执行结果为:

        192.168.1.107......connected

        192.168.1.107: Hello Server!

表示服务端成功接收到了客户端发送的数据。

代码说明:

        (1)  以上代码中使用的读取流,它所对应的数据源既不是键盘、也不是硬盘,而是属于特殊的内存——接收自网络的数据。

        (2)  服务端处理完某个客户端的请求以后,一定要关闭该客户端Socket服务,因为正如ServerSocket构造方法中所说,服务端同时能够处理的客户端数量是有限的,因此应尽快关闭掉闲置的客户端服务,以便处理其他客户端的请求。

        (3)  由于TCP传输协议是面向连接的,因此执行以上代码时,一定要先开启服务端Socket服务,再开启客户端Socket服务,否则由于指定服务端不存在而导致客户端连接失败。

 

需求2:客户端向服务端发送信息,服务端发送信息后,向客户端反馈信息,实现客户端与服务端之间的互访。

思路:

        客户端:

        (1)  创建Socket对象,初始化服务端IP地址和端口号,建立客户端Socket服务;

        (2)  获取Socket写入流,将数据写入到流中,以此将信息发送到服务端;

        (3)  获取Socket读取流,从流中读取服务端反馈的信息,并打印到控制台;

        (4)  关闭客户端Socket服务。

        服务端:

        (1)  创建ServerSocket对象,为其初始化一个端口号,建立服务端Socket服务;

        (2)  获取请求连接的客户端Socket对象,并获取到该客户端的IP地址;

        (3)  获取Socket读取流,接收客户端发送的信息;

        (4)  获取Socket写入流,向客户端反馈信息;

        (5)  关闭客户端Socket服务。

代码9:

import java.io.*;import java.net.*; classTcpClient{public static void main(String[] args) throws IOException{Socket clientSocket = newSocket(InetAddress.getLocalHost(), 10001); //获取Socket写入流,向服务端发送数据OutputStream out = clientSocket.getOutputStream(); byte[] buf = "HelloServer!".getBytes();out.write(buf, 0, buf.length); //获取Socket读取流,接收服务端反馈的信息,并打印到控制台InputStream in = clientSocket.getInputStream(); buf = new byte[1024];int len = in.read(buf);System.out.println("Server:"+new String(buf, 0, len)); //关闭客户端Socket服务clientSocket.close();}}classTcpServer{public static void main(String[] args) throws IOException{//建立服务端Socket服务ServerSocket serverSocket = newServerSocket(10001); //获取连接进来的客户端Socket对象,并获取客户端IP地址Socket clientSocket =serverSocket.accept();String clientIP =clientSocket.getInetAddress().getHostAddress(); System.out.println(clientIP+"...connected."); //获取Socket读取流,接收客户端发送的信息,打印到控制台InputStream in = clientSocket.getInputStream(); byte[] buf = new byte[1024];int len = in.read(buf); System.out.println(clientIP+":"+new String(buf, 0, len)); //获取Socket写入流,向客户端反馈信息OutputStream out = clientSocket.getOutputStream(); buf = "Connected Serversuccessfully.".getBytes();out.write(buf, 0, buf.length); //关闭客户端Socket服务clientSocket.close();}}
以上的代码的执行过程与代码8基本一致,执行后,服务端显示信息为:

        192.168.1.107...connected.

        192.168.1.107:Hello Server!

客户端显示信息为:

        ConnectedServer successfully.

 

需求3:

        建立一个文本转换服务器。客户端向服务端发送一段小写英文文本,服务端将其转换为大写后在反馈给客户端。客户端能够不断的通过键盘录入向服务端发送文本信息,直到用户输入“over”;服务端也能够不断的处理客户端请求,直到客户端断开连接。

思路:

        客户端:

        客户端Socket服务的建立,以及与服务端的连接过程与前两个例子基本一致,这里不再赘述。所不同的是,客户端向服务端发送的信息,将通过键盘输入。那么按照IO流的思想,数据源就是键盘录入,而数据目的就是网络写入流。并且我们将要操作的是文本,因此应使用字符流。当然,若需要提高效率我们还可以使用缓冲字符流。

        若要完成以上需求,在客户端中应创建三个IO流对象:用于键盘录入的字符读取流,用于向服务端发送文本数据的字符写入流,以及用于接收客户端反馈信息的字符读取流。

        基本的数据发送与接收流程为:开启键盘录入,循环获取用户键盘录入的字符文本,通过Socket写入流向服务端发型该文本,接着使用Socket读取流接收服务端发送的信息,并打印到控制台。

        服务端:

        服务端的数据源是Socket读取流,接收来自客户端的文本信息;而数据目的是Socket写入流,向客户端发送经大写转换后的文本。由于操作的同样均为文本,因此可以使用转换流将Socket读写流转换为字符读写流,并使用缓冲字符流封装起来,以提高效率。

        因此服务端基本的数据接收与发送流程为:通过Socket读取流循环接收来自客户端的小谢英文文本,并将其转换为大写后,使用Socket写入流发送到服务端。

代码10:

import java.io.*;import java.net.*; classTcpClient{public static void main(String[] args) throws IOException{Socket clientSocket = newSocket(InetAddress.getLocalHost(), 10002); InputStream in = clientSocket.getInputStream(); byte[] buf = new byte[1024];int len = in.read(buf);System.out.println(new String(buf,0, len)); //开启键盘录入BufferedReader bufr =new BufferedReader(newInputStreamReader(System.in));             //获取Socket写入流,并将其封装为缓冲字符写入流OutputStream out = clientSocket.getOutputStream();BufferedWriter bufw =new BufferedWriter(newOutputStreamWriter(out)); //将Socket读取流封装为字符流,用以接收服务端反馈的文本BufferedReader newBufr =new BufferedReader(newInputStreamReader(in)); //将键盘录入文本通过Socket写入流,发送给服务端String line = null;while((line = bufr.readLine()) !=null){if("over".equals(line))break; //向服务端发送文本bufw.write(line);//向客户端发送换行符,将此作为服务端Socket读取流的结束标记bufw.newLine();//刷新缓冲区,以便将数据写入到Socket流中bufw.flush(); //接收服务端反馈的文本,并打印到控制台line = newBufr.readLine();System.out.println(line);} //关闭键盘录入if(bufr != null)bufr.close();//关闭客户端Socket服务clientSocket.close();}}classTcpServer{public static void main(String[] args) throws IOException{ServerSocket serverSocket = newServerSocket(10002); Socket clientSocket = serverSocket.accept();System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected."); //获取Socket读取流和写入流InputStream in = clientSocket.getInputStream();OutputStream out = clientSocket.getOutputStream(); //将Socket读写流封装为字符读写流BufferedReader bufr =new BufferedReader(newInputStreamReader(in));BufferedWriter bufw =new BufferedWriter(newOutputStreamWriter(out)); bufw.write("Connectedsuccessfully.");bufw.flush(); //将客户端发送的小谢英文文本改写大写,并反馈给客户端String line = null;while((line = bufr.readLine()) !=null){bufw.write("Server:"+line.toUpperCase());bufw.newLine();//向客户端Socket读取流发送结束标记bufw.flush();//刷新缓冲区,将数据写入到Socket流中} //关闭客户端Socket服务clientSocket.close();}}
执行以上代码后,客户端命令行的显示结果为:

Connected successfully.

hello world!

Server:HELLOWORLD!

abc

Server:ABC

java

Server:JAVA

over

代码说明:

        (1)  Socket流本身是字节流,而本例中所操作的数据均为字符文本,因此需要通过转换流将其封装为字符流。

        (2)  为了方便操作文本,无论是客户端还是服务端,Socket写入流都被缓冲字符流封装了起来。根据缓冲字符写入流的特点,调用write方法仅仅是将字符文本数据写入到了缓冲区中,而未真正写入到Socket流中,因此一定要在写入数据以后进行刷新操作,否则对方将无法接受到任何信息。由于读取流的read方法为阻塞式方法,当没有接受到任何数据时将一直处于等待状态。

        (3)  缓冲字符读取流的readLine方法是以换行符(’\n’)、回车符(’\r’)或者回车后接换行(”\r\n”)作为读取结束标记的,因此在通过Socket写入流写入一行文本以后,一定要在该行文本末尾写入结束标记,或者调用newLine方法亦可。否则,对方读取流未读取到任何结束标记,同样将一直处于等待状态。

        (4)  执行客户端代码过程中,若输入“over”将退出循环键盘录入,进而关闭客户端Socket服务,以此同时,服务端也将结束服务并退出。但实际上,在这一过程中,我们并没有为服务端定义结束标记,那么服务端为什么会自定关闭呢?当在客户端调用close方法,结束客户端Socket服务的同时,将向该客户端Socket流中写入“-1”。虽然,Socket读取流被封装为了缓冲字符读取流,但是其底层依旧是字节流,因此读取到-1后就将其作为结束标记,结束了读取操作,因此服务端退出了循环读取操作。

 

小知识点2:

        为了简化代码,进行写入操作时(也就是向对方发送信息时),可以使用PrintWriter对象。该对象既可以封装字节流,也可以封装字符流,并且将模式设定为“true”后,调用它的println方法,可以自动完成换行与刷新操作,大大简化了代码书写。参考代码如下,

PrintWriter out = new PrintWriter(Socket.getOutputStream(), “true”);

out.println(“文本内容”);//同时完成换行与刷新。

 

需求4:通过TCP传输服务,实现从客户端向服务端上传文件的功能。

思路:

        实现这一需求的基本思路与需求3基本一致,所不同的主要是数据传输时的数据源和目的发生了变化。这一需求中客户端的数据源不再是键盘录入,而是硬盘文本文件;相应的,服务端的数据目的也不再是控制台,而是服务端的硬盘文本文件。

代码11:

import java.io.*;import java.net.*; classTcpClient{public static void main(String[] args) throws IOException{Socket clientSocket = new Socket(InetAddress.getLocalHost(),10001); InputStream in = clientSocket.getInputStream();OutputStream out =clientSocket.getOutputStream(); //将Socket读取流封装为缓冲字符读取流,接收服务端发送的连接信息BufferedReader bufr =new BufferedReader(newInputStreamReader(in));             String msg = bufr.readLine();System.out.println(msg); //创建缓冲字符读取流,并与被上传文件进行关联bufr =new BufferedReader(newFileReader("E:\\IPDemo.java"));             //将Socket写入流封装为打印写入流,用于发送文本文件PrintWriter pw =new PrintWriter(out, true); while((msg = bufr.readLine()) !=null){pw.println(msg);} bufr.close();//向服务端发送接收标记pw.println("over"); //通过Socket读取流读取客户单发送的文件上传完毕信息bufr =new BufferedReader(newInputStreamReader(in));msg = bufr.readLine();System.out.println(msg); clientSocket.close();}}classTcpServer{public static void main(String[] args) throws IOException{ServerSocket serverSocket = newServerSocket(10001); Socket clientSocket = serverSocket.accept();System.out.println(clientSocket.getInetAddress().getHostAddress()+"...connected."); InputStream in = clientSocket.getInputStream();OutputStream out =clientSocket.getOutputStream(); //将Socket读取流封装为缓冲字符读取流,用于接收客户端发送的文本内容BufferedReader bufr =new BufferedReader(newInputStreamReader(in));//将Socket写入流封装为打印写入流,向客户端发送连接信息PrintWriter pw =new PrintWriter(out, true);pw.println("Successfullyconnected."); /*创建一个打印写入流,并与服务端的一个文本文件进行关联将从客户端接收的文本内容写入到该文件中*/pw =new PrintWriter(newFileWriter("E:\\IPDemo_copy.java"), true); String line = null;while((line = bufr.readLine()) !=null){//判断结束标记,当客户端发送"over"时,结束读取if("over".equals(line))break; pw.println(line);} pw.close(); //通过Socket写入流,向客户端发送文件上传完毕信息pw =new PrintWriter(out, true);pw.println("Uploadcompelete."); clientSocket.close();}}
执行以上代码以后,客户端的命令行显示结果为:

Successfullyconnected.

Uploadcompelete.

服务端命令行显示结果为:

        169.254.100.231...connected.

并且在指定路径下,创建了原文件的复制版本。

代码说明:

        (1)  客户端在结束发送数据以后,继续向服务端发送了内容为“over”的字符串。其实这个字符串的作用相当于结束标记,那么当服务端判断出接收的信息为“over”时就会退出数据读取循环,进而正常指向下面的代码。相反,如果客户端没有向服务端发送结束标记,那么后者就会一直处于等待读取状态,与此同时,客户端由于继续向下执行,同样会等待服务端发送“文件上传成功”信息,最终导致双方互相等待,出现类似死锁的情况。而这个问题之所以没有发生在之前的代码中是因为,每当客户端结束向服务端发送信息后,紧接着就会关闭客户端Socket服务,这一动作会同时关闭这两个流。此时,由于服务端接收到结束标记,从而结束了从客户端接收数据的操作。

        (2)  在实际开发中,需要对以上代码分别进行try-catch异常处理,而不能仅仅声明抛出异常了事。比如,对创建Socket对象的代码就需要单独进行异常处理,因为如果未能成功建立网络,也就没有继续向下执行的必要了,并且应该报出诸如“未能成功连接服务端”的异常信息。这里仅仅为演示方便而略去了异常处理代码。

        (3)  虽然我们在定义服务端代码的时候,直接为服务端文件定义了与客户端文件相同的文件名,但实际上,服务端不可能知道文件名,因此需要客户端通过Socket写入流发送给服务端。为了防止客户端因此重发发送而覆盖同名文件,服务端在创建文件时,可以在原文件名基础上加上序号,或者时间戳(也就是文件创建时间),以示区分。

 

小知识点3:

        虽然,客户端通过向服务端发送内容为“over”的字符串作为结束标记,但是这种方法在实际开发中会存一个问题。客户端向服务端发送一个文本文件,内容中恰好有一个单词“over”单独占据一行,那么服务端接收到该数据后就会误认为接收到结束标记而结束读取数据,造成数据丢失。因此上述需求4中对结束标记的解决方法并不好。

        那么这里就可以使用Socket类为我们提供的方法——shutdownOutput()来解决这个问题。该方法的作用就是向数据接收方的Socket读取流中发送一个结束标记,令其停止读取操作。这样就可以避免由于双方均在等待对方发送数据而造成的程序停止执行的问题。

        只需将代码11中,客户端的“pw.println("over")”修改为“clientSocket.shutdownOutput()”,并将服务端的数据接收循环中的if判断语句删除即可。执行后的结果与前述结果相同,并且同样可以完成文件的上传功能。

        但是这个方法也不是十全十美的,因为调用该方法在向对方发送结束标记的同时,会关闭本方的Socket写入流,因此数据发送方在调用该方法以后不能继续发送数据了,相当于起到了close方法一半的作用——保留了Socket读取流。

0 0
原创粉丝点击