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