1、317第 11 章 网络编程有人说,20 世纪最伟大的发明并不是计算机,而是计算机网络。还有人说,如果你买了计算机而没有联网,就等于买了电话机却没有接电话线一样。计算机网络就是实现了多个计算机互联的系统,相互连接的计算机之间彼此能够进行数据交换。正如城市道路系统总是伴随着城市交通规则来使用的道理,计算机网络总是伴随着计算机网络协议一起使用的。网络协议规定了计算机之间连接的物理、机械(网线与网卡的连接规则) 、电气(有效的电平范围)等特性以及计算机之间的相互寻址规则、数据发送冲突的解决、长的数据如何分段传送与接收等。就象不同的城市可能有不同的交通规则一样,目前的网络协议也有多种,其中,TCP/I
2、P 协议就是一个非常实用的网络协议,它是 Internet 所遵循的协议,是一个“既成事实”的标准,已广为人知并且广泛应用在大多数操作系统上,也可用于大多数局域网和广域网上。网络应用程序,就是在已实现了网络互联的不同的计算机上运行的程序,这些程序相互之间可以交换数据。编写网络应用程序,首先必须明确网络程序所要使用的网络协议,TCP/IP 是网络应用程序的首选协议,大多数网络程序都是以这个协议为基础,本章关于网络程序编写的讲解,都是基于 TCP/IP 协议的。11.1 网络编程的基础知识11.1.1 TCP/IP 网络程序的 IP 地址和端口号要想让网络中的计算机能够互相通信,必须为每台计算机指
3、定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送数据的计算机,在 TCP/IP 协议中,这个标识号就是 IP 地址,目前 IP 地址在计算机中用四个字节,也就是 32 位的二进制数来表示,称为 Ipv4。为了便于记忆和使用,我们通常取用每个字节的十进制数,并且每个字节之间用圆点隔开的文本格式来表示 IP 地址,如 192.168.8.1。随着计算机网络规模的不断扩大,用四个字节来表示 IP 地址已越来越不敷使用,人们正在实验和定制使用 16 个字节表示 IP 地址的格式,这就是 Ipv6。由于 Ipv6 还没有投入使用,现在网络上用的还都是 Ipv4,我们这里的知识也只围绕着 I
4、pv4 来展开。因为一台计算机上可同时运行多个网络程序,IP 地址只能保证把数据送到该计算机,但不能保证把这些数据交给哪个网络程序,因此,每个被发送的网络数据包的头部都包含有一个称为“端口“ 的部分,它是一个整数,用于表示该数据帧交给哪个应用程序来处理。我们还必须为网络程序指定一个端口号,不同的应用程序接收不同端口上的数据,同一台计算机上不能有两个使用同一端口的程序运行。端口数范围为 0-65535 之间。0-1023 之间的端口数是用于一些知名的网络服务和应用,用户的普通网络应用程序应该使用 1024 以上的端口数,从而避免端口号已被另一个应用或系统服务所用。如果我们的一个网络程序指定了自己
5、所用的端口号为 3150,那么其他网络程序发送给这个网络程序的数据包中必须指明接收程序的端口号为 3150,当数据到达第一个网络程序所在的计算机后,驱动程序根据数据包中的 3150 这个端口号,就知道要将这个数据包交给这个网络程序。11.1.2 UDP 与 TCP在 TCP/IP 协议栈中,有两个高级协议是我们网络应用程序编写者应该了解的,它们是“传输控制协议“(Transmission Control Protocol,简称 TCP)和“用户数据报协议“(User Datagram Protocol,简称 UDP) 。318TCP 是面向连接的通信协议,TCP 提供两台计算机之间的可靠无错的
6、数据传输。应用程序利用TCP 进行通信时,源和目标之间会建立一个虚拟连接。这个连接一旦建立,两台计算机之间就可以把数据当作一个双向字节流进行交换。就像我们打电话一样,互相能听到对方的说话,也知道对方的回应是什么。UDP 是无连接通信协议,UDP 不保证可靠数据的传输,但能够向若干个目标发送数据,接收发自若干个源的数据。简单地说,如果一个主机向另外一台主机发送数据,这一数据就会立即发出,而不管另外一台主机是否已准备接收数据。如果另外一台主机收到了数据,它不会确认收到与否。就像传呼台给用户发信息一样,传呼台并不知道你是否能收到信息(为了避免丢失用户信息,他们常常将一条信息发送两遍) 。TCP、UD
7、P 数据包(也叫数据帧)的基本格式如图 11.1 所示。图 11.111.1.3 Socket大家不要生硬和孤立地去理解什么是 Socket,就象我们不要让一个从来没有见到过大米与米饭的人去理解什么是“rice”一样的道理,任何一个事物和概念都得有个代名词,大家只有先理解和事物和概念本身,就自然理解了它的代名词。同样 Socket 是网络驱动层提供给应用程序编程的接口和一种机制,大家先掌握和理解了这个机制,自然就明白了什么是 Socket。大家可以认为 Socket 是应用程序创建的一个港口码头,应用程序只要把装着货物的集装箱(在程序中就是要通过网络发送的数据)放到港口码头上,就算完成了货物的
8、运送,剩下来的工作就由货运公司去处理了(在计算机中由驱动程序来充当货运公司) 。对接收方来说,应用程序也要创建的一个港口码头,然后就一直等待到该码头的货物到达,最后从码头上取走货物(发给该应用程序的数据) 。 Socket 在应用程序中创建,通过一种绑定机制与驱动程序建立关系,告诉自己所对应的 Ip和 Port。此后,应用程序送给 Socket 的数据,由 Socket 交给驱动程序向网络上发送出去。计算机从网络上收到与该 Socket 绑定的 IP+Port 相关的数据后,由驱动程序交给 Socket,应用程序便可从该 Socket 中提取接收到的数据。网络应用程序就是这样通过 Socket
9、 进行数据的发送与接收的。作者用下面的两个图例来帮助读者理解应用程序、Socket、网络驱动程序之间的数据传送过程与工作关系。1数据发送过程如图 11.2 所示:319图 11.22数据接收过程如图 11.3 所示:图 11.3Java 分别为 UDP 和 TCP 两种通信协议提供了相应的编程类,这些类存放在 包中,与 UDP 对应的是 DatagramSocket,与 TCP 对应的是 ServerSocket(用于服务器端)和 Socket(用于客户端) 。网络通信,更确切的说,不是两台计算机之间在收发数据,而是两个网络程序之间在收发数据,我们也可以在一台计算机上进行两个网络程序之间的通
10、信,这两个程序要使用不同的端口号。32011.2 Java 编写 UDP 网络程序11.2.1 DatagramSocket编写 UDP 网络程序,我们首先要用到 .DatagramSocket 类,通过查阅 JDK 文档资料,看到 DatagramSocket 类的构造函数主要有如下几种形式:public DatagramSocket() throws SocketExceptionpublic DatagramSocket(int port) throws SocketExceptionpublic DatagramSocket(int port,InetAddress laddr) th
11、rows SocketException用第一个构造函数创建 DatagramSocket 对象,没有指定端口号,系统就会为我们分配一个还没有被其他网络程序所使用的端口号。用第二个构造函数创建 DatagramSocket 对象,我们就可以指定自己想要的端口号。用第三个构造函数创建 DatagramSocket 对象,我们除了指定自己想要的端口号外,还可以指定相关的 IP 地址,这种情况适用于计算机上有多块网卡和多个 IP 的情况,我们可以明确规定我们的数据通过哪块网卡向外发送和接收哪块网卡收到的数据。如果在创建DatagramSocket 对象时,我们没有指定网卡的 IP 地址,在发送数据时
12、,底层驱动程序会为我们选择其中一块网卡去发送,在接收数据时,我们会接收到所有网卡收到的与程序端口一致的数据,对于我们一般只有一块网卡的情况,我们就不用专门指定了,发送和接收时肯定都是它了。其实,对于只有一块网卡的情况,在这里指定了 IP 地址,反而会给我们的程序带来很大的不方便,你的这个网络程序只能在具有这个 IP 地址的计算机上运行,而不能在其他的计算机上运行。当我们编写发送程序时,用哪个构造函数呢?我们在创建时 DatagramSocket 对象时,不指定端口号,系统就会为我们分配一个端口号,因此,我们可以用第一个构造函数,这样就相当于你给别人打电话时,你的电话可以是任意的,最好不要固定,
13、如果你非要用某个电话,那当别人正在用这个电话时,你就只有干等的份了。但作为接收程序,我们必须自己指定一个端口号,而不要让系统随机分配,我们可以用第二个构造函数,否则,我们就不能在程序运行前知道我们的端口号,并且每一次运行所分配的端口号都不一样,就象有朋友让你给他打电话,可他的电话号码不确定是不行的。 如果我们的程序不再使用某个 Socket,我们应该调用 DatagramSocket.close()方法,关闭这个 Socket,通知驱动程序释放为这个 Socket 所保留的资源,系统就可以将这个 Socket 所占用的端口号重新分配给其他程序使用。在发送数据时,我们用 Datagram.sen
14、d()方法,其完整的格式如下:public void send(DatagramPacket p) throws IOException在要接收数据时,我们用 Datagram.receive()方法,其完整的格式如下:public void receive(DatagramPacket p) throws IOExceptionDatagram.send()和 Datagram.receive()方法都需要我们传递一个 DatagramPacket 类的实例对象,如果把 DatagramSocket 比作创建的港口码头,那么 DatagramPacket 就是我们发送和接收数据的集装箱。11
15、.2.2 DatagramPacket查阅 JDK 文档,DatagramPacket 类的构造函数主要有如下几种形式:public DatagramPacket(byte buf,int length)public DatagramPacket(byte buf,int length,InetAddress address,int port)用第一个构造函数创建的 DatagramPakcet 对象,只指定了数据包的内存空间和大小,相当于只定义了集装箱的大小。用第二个构造函数创建的 DatagramPacket 对象,不仅指定了数据包的内存空间和大小,而且指定了数据包的目标地址和端口。在接收
16、数据时,我们是没法事先就知道哪321个地址和端口的 Socket 会给我们发来数据,就象我们要准备一个集装箱去接收发给我们的货物时,是不用标明发货人或是收货人的地址信息的,所以我们应该用第一个构造函数来创建接收数据的DatagramPakcet 对象。在发送数据时,我们必须指定接收方 Socket 的地址和端口号,就象我们要发送数据的集装箱上面必须标明接收人的地址信息一样的道理,所以我们应该用第二个构造函数来创建发送数据的 DatagramPakcet 对象。11.2.3 InetAddress在发送数据时,DatagramPacket 构造方法需要我们传递一个 InetAddress 类的实
17、例对象,InetAddress 是用于表示计算机地址的一个类,我们习惯上表示计算机地址是用“192.168.0.1”或“www.it315.org”的字符串格式,我们现在要做的就是根据这种习惯上的字符串地址格式来创建一个 InetAddress 类的实例对象,查阅 JDK 文档资料资料,我们发现 InetAddress.getByName()这个静态方法能够根据我们的条件返回一个 InetAddress 类的实例对象。另外,当我们将数据接收到 DatagramPacket 对象中后,我们想知道发送方的 IP 地址和端口号,该怎么办呢?到现在为止,我们应该学会了解决类似这样的小问题的最基本的思路
18、了,大家应该很容易想到在 JDK 文档中去查 DatagramPacket 类的方法,看其中有没有解决我们问题的方法。在 JDK 文档中,我们又看到了 DatagramPacket.getInetAddress()和 DatagramPacket.getPort()方法。getInetAddress 方法返回的是 InetAddress 类型的对象,我们需要将它转换成用点(.)隔开的字符串型的 IP 地址。在 JDK 文档中去查 InetAddress 类的帮助,我们又可以看到InetAddress.getHostAddress 方法能够以字符串的形式返回 InetAddress 对象中的 I
19、P 地址。11.2.4 最简单的 UDP 程序有了前面这些网络编程的基本知识,我们接下来编写两个最简单的 UDP 程序,在一台计算机上相互发送和接收数据,接收程序所用的端口号为 3000,发送程序的端口号由系统分配,这里假设运行程序的计算机的 IP 地址是 192.168.0.213,读者应根据将程序中的这个地址,修改成你的计算机的实际地址后,编译运行。发送程序:UdpSend.javaimport .*;public class UdpSendpublic static void main(String args) throws ExceptionDatagramSocket ds=new
20、DatagramSocket();String str=“hello world”;DatagramPacket dp=new DatagramPacket(str.getBytes(),str.length(),InetAddress.getByName(“192.168.0.213”),3000);ds.send(dp);ds.close();接收程序:UdpRecv.javaimport .*;public class UdpRecv322public static void main(String args) throws ExceptionDatagramSocket ds=new
21、DatagramSocket(3000);byte buf=new byte1024;DatagramPacket dp=new DatagramPacket(buf,1024);ds.receive(dp);String strRecv=new String(dp.getData(),0,dp.getLength() + “ from ” + dp.getAddress().getHostAddress()+”:”+dp.getPort(); System.out.println(strRecv);ds.close();由于创建 DatagramPacket 时,要求的数据格式都是 byte
22、 型的数组,所以程序在发送数据时用到了 String.getBytes()方法将字符串转换成 byte 型的数组,在接收数据时用到了 String 类的public String(byte bytes, int offset, int length)构造方法,将 byte 型的数组转换成字符串。我们为什么不用 public String(byte bytes)构造方法来将 byte 型的数组转换成字符串呢?因为我们在接收数据前,是没法知道对方实际发送的数据包的长度的,因此,在程序中定义buf 数组具有 1024 个字节,即表示我们能够接收的数据包的大小最多为 1024 个字节,也就是确信对方每
23、次发送的数据包不会超过 1024 个字节的。对方发送的数据的大小是不确定的,往往都不可能正好是 1024 个字节,如上面程序中,我们只收到的“hello world”,只有 11 个字节的数据,public String(byte bytes)是将数组中的所有元素都转换成字符串,即将这 1024 个字节都转换成字符串,包括那些根本没有被添充的单元。public String(byte bytes, int offset, int length)是将字节数组中从 offset 开始,往后一共 length 个单元的内容转换成字符串,DatagramPacket.getLength()方法可以返回
24、数据包中实际收到的字节数。所以,接收程序中的“String strRecv=new String(dp.getData(),0,dp.getLength() + “ from “ + dp.getAddress ().getHostAddress()+”:”+dp.getPort(); ”语句将接收到的数据转换成字符串,并在后面加上发送方的地址和端口。指点迷津:UDP 数据的发送,类似发送寻 呼一样的道理, 发送者将数据 发送出去就不管了,是不可靠的,有可能在发送的过程中发生数据丢失。就象 寻呼机必须先处 于开机接收状态才能接收寻呼一样的道理,我们要先运行 UDP 接收程序,再运行 UDP发送
25、程序, UDP 数据包的接收是 过期作废的。因此,前面的接收程序要比发送程序早运行才行,你 调试成功了吗?当 UDP 接收程序运行到 DatagramSocket.receive 方法接收数据时,如果还没有可以接收的数据,在正常情况下,receive 方法将阻塞,一直等到网络上有数据到来,receive 接收该数据并返回。如果网络上没有数据发送过来, receive 方法也没有阻塞,肯定是你前面的程序出现了问题,通常都是使用了一个还在被其他程序占用的端口号,你的 DatagramSocket绑定没有成功。这两个网络程序当然也可以在两台计算机上运行,但要将发送方发送数据的目标 IP设置成接收数据
26、的计算机的 IP 地址。多学两招:如果将 UdpSend 程序中发送的内容改为中文,如 “我的程序”,接收到的内容有问题, 请先想想为什么?323因为一个中文字符转换为字节时占用两个字节大小,而一个英文字符转换为字节时只有一个字节大小,所以,应将发送程序中的DatagramPacket dp=new DatagramPacket(str.getBytes(),str.length(),InetAddress.getByName(),3000); 修改为:DatagramPacket dp=new DatagramPacket(str.getBytes(),str.getBytes().leng
27、th,InetAddress.getByName(),3000);就行了。也就是说,在指定发送数据包的大小时,应按字节数组的大小来计算,而不是字符串中字符的个数。11.2.5 用 UDP 编写聊天程序掌握了 UDP 网络程序编写的基本过程,我们就可以结合前面的多线程、GUI 来编写一个更完善的网络应用程序,这个程序具有图形用户界面,如图 11.4 所示:图 11.4这个程序即可以发送数据,也可以接收数据。在实际开发中,我们通常都会将一个大的问题分成若干小的问题来解决,对与上面这个程序,我们将其分为三个步骤来完成:1. 编写图形用户界面部分2. 编写网络消息发送部分3. 编写网络消息接收部分首先
28、,我们编写图形用户界面部分,程序代码如下:import java.awt.*;import java.awt.event.*;324public class Chat Frame f=new Frame(“我的聊天室“);TextField tfIP=new TextField(15);/* tfIP 是用于输入 IP 地址的文本框,在发送数据时,要取出其中的 IP 地址,所以将其定义成员变量,以便发送消息的程序代码访问。*/List lst=new List(6);/*lst 是用于显示接收消息的列表框,在接收到数据时,要向其中增加新的记录项,所以将其定义成员变量,以便接收消息的程序代码访问
29、。*/public static void main(String args)Chat chat=new Chat();chat.init();public void init()f.setSize(300,300);f.add(lst);Panel p=new Panel();p.setLayout(new BorderLayout();p.add(“West“,tfIP);TextField tfData=new TextField(20);p.add(“East“,tfData);f.add(“South“,p);f.setVisible(true);f.setResizable(fal
30、se);/限制用户改变窗口的大小/增加关闭窗口的事件处理代码f.addWindowListener(new WindowAdapter()public void windowClosing(WindowEvent e)f.setVisible(false);f.dispose();System.exit(0););/增加在消息文本框中按下回车键的事件处理代码tfData.addActionListener(new ActionListener()public void actionPerformed(ActionEvent e)325/要在这里增加网络消息发送相关程序代码/下面的语句用于数据发
31、送后,清空文本框中原来的内容(TextField)e.getSource().setText(“););编译并运行上面的程序,检查程序是否已经完成了图形用户界面的需求。我们接着编写网络消息发送部分的程序代码,这里用黑体标记新加的相关代码,以便读者于原来的代码相区别。import java.awt.*;import java.awt.event.*;import .*;public class Chat Frame f=new Frame(“我的聊天室“);TextField tfIP=new TextField(15);List lst=new List(6);DatagramSocket d
32、s;/*由于 DatagramSocket 的构造函数声明可能抛出异常,我们的程序需要用 trycatch 语句进行异常捕获处理,所以我们不能直接在这里调用 DatagramSocket 的构造函数对 ds 进行初始化,我们需要将ds 的初始化放在 Chat 类的构造函数中去完成。*/public Chat()tryds=new DatagramSocket(3000);catch(Exception ex)ex.printStackTrace();public static void main(String args)Chat chat=new Chat();chat.init();publ
33、ic void init()f.setSize(300,300);f.add(lst);Panel p=new Panel();p.setLayout(new BorderLayout();p.add(“West“,tfIP);TextField tfData=new TextField(20);p.add(“East“,tfData);f.add(“South“,p);326f.setVisible(true);f.setResizable(false);/限制用户改变窗口的大小/增加关闭窗口的事件处理代码f.addWindowListener(new WindowAdapter()publ
34、ic void windowClosing(WindowEvent e)ds.colse();/程序退出时,关闭 Socket,释放相关资源f.setVisible(false);f.dispose();System.exit(0););/增加在消息文本框中按下回车键的事件处理代码tfData.addActionListener(new ActionListener()public void actionPerformed(ActionEvent e)/取出文本框中的消息字符串,并将其转换成字节数组byte buf;buf = e.getActionCommand().getBytes();D
35、atagramPacket dp= new DatagramPacket(buf,buf.length,InetAddress.getByName(tfIP.getText(),3000);tryds.send(dp);catch(Exception ex)ex.printStackTrace();/*上面的 Exception 的引用变量名不能为 e,而是改写成了 ex,因为 e 已经在actionPerformed 方法中作为形式参数变量名被定义过了。*/(TextField)e.getSource().setText(“););我们接着编写网络消息接收部分的程序代码,接收程序代码在一个新
36、的线程中完成,这样,在接收处于阻塞状态时,不会影响到程序的发送部分。新增加的代码为黑体显示部分。import java.awt.*;import java.awt.event.*;import .*;public class Chat Frame f=new Frame(“我的聊天室“);TextField tfIP=new TextField(15);327List lst=new List(6);DatagramSocket ds;。public Chat()tryds=new DatagramSocket(3000);catch(Exception ex)ex.printStackTra
37、ce();new Thread(new Runnable()public void run()byte buf=new byte1024; DatagramPacket dp= new DatagramPacket(buf,1024); while(true)tryds.receive(dp);lst.add(new String(buf,0,dp.getLength()+“:from“+dp.getAddress().getHostAddress(),0);catch(Exception e)e.printStackTrace(); ).start();在上面的程序中,我们使用的是 List
38、 的 add(String item,int index)方法将接收到的消息增加到列表框中,将 index 的值设置为 0,我们可以将最后接收到的消息作为列表框中的第一条记录项显示,为用户提供更友好方便的界面。编译上面完整的程序,我们就实现了一个具有图形用户界面和收发功能的聊天程序,我们怎样来测试我们的这个程序是否正确呢?在这里,我向大家讲解另外两个小问题,顺便测试我们程序。第一个问题是:我们这个网络程序能够自己给自己发送数据吗?当然可以,就象一个人非常孤单,自己可以给自己写信,一个网络程序也是可以给自己发送数据的,大家只要将上面的 IP 文本框中的目标 IP 指向自己的主机,你就能够收到自己
39、给自己发送的数据了。第二个问题是:如何发送广播数据?只能在同一个网段中发送广播数据,将该网段 IP 地址的主机号部分的每个二进制位都设置为 1,这个 IP 地址就是这个网段的广播地址了,如果我们发送数据的目标地址是这个网段的广播地址,这个网段上的所有主机都可以接收到发送的数据。作者的主机所在的网络号为 192.168.0,子网掩码是 255.255.255.0,所以,这个网段的广播地址就是192.168.0.255。顺便给大家补充点儿网络方面的知识,如果作者所在网络的子网掩码是255.255.254.0,那么这个网段的广播地址就是 192.168.1.255,读者要将上面的 IP 文本框中的目
40、标 IP 指向自己网段的广播地址,你也能收到你发送的数据,因为不管发送方是谁,只要是广播数328据,你的主机都能接收的。如果读者有多台计算机的网络环境,你可以试试多台计算机之间收发数据的情况,只要你在一台计算机上运行正常了,在多台计算机上也不会有什么问题的。作者在教学中,碰到有的学员问过这样的问题,他的接收程序代码没有放在一个新的线程中,而是直接在 main 方法中调用,如下所示:public static void main(String args)Chat chat=new Chat();chat.init();chat.run();public void run()byte buf=ne
41、w byte1024; DatagramPacket dp= new DatagramPacket(buf,1024); while(true)tryds.receive(dp);lst.add(new String(buf,0,dp.getLength()+“:from“+dp.getAddress().getHostAddress(),0);catch(Exception e)e.printStackTrace(); 程序也能运行得很好。程序并没有在接收部分阻塞,这是怎么回事呢?这是因为系统为我们创建了一个 AWT 线程,发送数据的程序代码正好在这个 AWT 线程上运行,而接收数据的程序代
42、码就直接在 main 方法运行的线程上运行了,可见,发送和接收部分还是在两个不同的线程上运行的。11.3 Java 编写 TCP 网络程序利用 UDP 通信的两个程序是平等的,没有主次之分,两个程序代码可以是完全一样的。利用TCP 协议进行通信的两个应用程序,是有主从之分的,一个称为服务器程序,另外一个称为客户机程序,两者的功能和编写方法大不一样。TCP 服务器程序类似 114 查号台,而 TCP 客户机程序类似普通电话。必须先有 114 查号台,普通电话才能拨打 114,在 114 查号台那边是先有一个总机,总机专门用来接听拨打进来的电话,并不与外面的电话直接对话,而是将接进来的电话分配到一
43、个空闲的座机上,然后由这个座机去与外面的电话直接对话。总机在没有空闲的座机时,可以让对方排队等候,但等候服务的电话达到一定数量时,总机就会彻底拒绝以后再拨打进来的电话。Java 中提供的 ServerSocket 类用于完成类似 114 查号台总机的功能,Socket 类用于完成普通电话和 114 查号台端的座机功能。这个交互的过程如图 11.5 所示:329图 11.51).服务器程序创建一个 ServerSocket,然后调用 accept 方法等待客户来连接。2).客户端程序创建一个 Socket 并请求与服务器建立连接。3).服务器接收客户的连接请求,并创建一个新的 Socket 与该
44、客户建立专线连接。4).刚才建立了连接的两个 Socket 在一个单独的线程(由服务器程序创建)上对话。5).服务器开始等待新的连接请求。11.3.1 ServerSocket编写 TCP 网络服务器程序时,我们首先要用到 .ServerSocket 类用以创建服务器Socket,通过查阅 JDK 文档资料,看到 ServerSocket 类的构造函数有如下几种形式:public ServerSocket() throws IOExceptionpublic ServerSocket(int port) throws IOExceptionpublic ServerSocket(int por
45、t,int backlog) throws IOExceptionpublic ServerSocket(int port,int backlog,InetAddress bindAddr) throws IOException用第一个构造函数创建 ServerSocket 对象,没有与任何端口号绑定,不能被直接使用,还要继续调用 bind 方法,才能完成其他构造函数所完成的功能。用第二个构造函数创建 ServerSocket 对象,我们就可以将这个 ServerSocket 绑定到一个指定的端口上,就象为我们的呼叫中心安排一个电话号码一样,如果在这里指定的端口号为 0,系统就会为我们分配一个
46、还没有被其他网络程序所使用的端口号,作为服务器程序,端口号必须事先指定,其他客户才能根据这个号码进行连接,所以将端口号指定为 0 的情况并不常见。用第三个构造函数创建 ServerSocket 对象,就是在第二个构造函数的基础上,我们根据backlog 参数指定, 在服务器忙时,可以与之保持连接请求的等待客户数量,对于第二个构造函数,没有指定这个参数,则使用默认的数量,大小为 50。用第四个构造函数创建 ServerSocket 对象,我们除了指定第三个构造函数中的参数外,还可以指定相关的 IP 地址,这种情况适用于计算机上有多块网卡和多个 IP 的情况,我们可以明确规330定我们的 Serv
47、erSocket 在哪块网卡或 IP 地址上等待客户的连接请求,在前面几个构造函数中,都没有指定网卡的 IP 地址,底层驱动程序会为我们选择其中一块网卡或一个 IP 地址,显然,对于我们一般只有一块网卡的情况,我们就不用专门指定 IP 地址了。我们在前面的 DatagramSocket部分就讲过,对于只有一块网卡的情况,在这里指定了 IP 地址,反而会给我们的程序带来很大的不方便,你的这个网络程序只能在具有这个 IP 地址的计算机上运行,而不能在其他的计算机上运行。看完了上面几个 ServerSocket 构造函数的各自作用,对于通常情况的应用,我们不难作出选择,第二个构造方法来创建我们的 S
48、erverSocket 对象是非常合适和方便的。11.3.2 Socket客户端要与服务器建立连接,首先必须创建一个 Socket 对象,查阅 JDK 文档资料,看到Socket 类的构造函数有如下几种形式:public Socket()public Socket(String host,int port) throws UnknownHostException,IOExceptionpublic Socket(InetAddress address,int port) throws IOExceptionpublic Socket(String host,int port,InetAddre
49、ss localAddr,int localPort)throws IOExceptionpublic Socket(InetAddress address,int port,InetAddress localAddr,int localPort) throws IOException用第一个构造函数创建 Socket 对象,不与任何服务器建立连接,不能被直接使用,需要调用connect 方法,才能完成和其他构造函数一样的功能。如果我们想用同一个 Socket 对象,去轮循连接多个服务器,可以使用这个构造函数创建 Socket 对象后,再不断调用 connect 方法去连接每个服务器。用第二个和第三个构造函数创建 Socket 对象后,会根据参数去连接在特定地址和端口上运行的服务器程序,第二个构造函数接受字符串格式的地址,第三个构造函数接受 InetA