67.JAVA编程思想——套接字

67.JAVA编程思想——套接字

“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。

在Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个InputStream以及OutputStream(若使用恰当的转换器,则分别是Reader和Writer),以便将连接作为一个IO 流对象对待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及Socket,客户用它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字”连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用getInputStream()以及getOutputStream()从每个套接字产生对应的InputStream
和OutputStream 对象。这些数据流必须封装到缓冲区内。可对类进行格式化,就象对待其他任何流对象那样。对于Java 库的命名机制,ServerSocket(服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可能认为ServerSocket 最好叫作“ServerConnector”(服务器连接器),或者其他什么名字,只是不要在其中安插一个“Socket”。也可能以为ServerSocket 和Socket 都应从一些通用的基础类继承。事实上,这两种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基础类。相反,ServerSocket
的主要

任务是在那里耐心地等候其他机器同它连接,再返回一个实际的Socket。这正是“ServerSocket”这个命名不恰当的地方,因为它的目标不是真的成为一个Socket,而是在其他人同它连接的时候产生一个Socket 对象。

然而,ServerSocket 确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接,然后利用accept()方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求,不能接收实际的数据包。所以尽管ServerSocket 对于编程并无太大的意义,但它确实是“物理性”的。

创建一个ServerSocket 时,只需为其赋予一个端口编号。不必把一个IP 地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个Socket 时,却必须同时赋予IP 地址以及要连接的端口编号(另一方面,从ServerSocket.accept()返回的Socket 已经包含了所有这些信息)。

1     一个简单的服务器和客户机程序

将以最简单的方式运用套接字对服务器和客户机进行操作。服务器的全部工作就是等候建立一个连接,然后用那个连接产生的Socket 创建一个InputStream 以及一个OutputStream。在这之后,它从InputStream 读入的所有东西都会反馈给OutputStream,直到接收到行中止(END)为止,最后关闭连接。

客户机连接与服务器的连接,然后创建一个OutputStream。文本行通过OutputStream 发送。客户机也会创建一个InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连接,否则程序不能工作——尽管实际并不通过那个网络通信)。

1.1     服务端代码

import java.io.*;

import java.net.*;

public
class
JabberServer {

// Choose aport outside of the range 1-1024:

public
staticfinalintPORT
= 8080;

public
staticvoid
main(String[]
args)throwsIOException {

ServerSocket
s = new ServerSocket(PORT);

System.out.println("Started: " +
s);

try {

// Blocks until a connection occurs:

Socket socket =
s.accept();

try {

System.out.println("Connection accepted: " +
socket);

BufferedReader
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// Output is automatically flushed

// by PrintWriter:

PrintWriter
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),

true);

while (true) {

String
str = in.readLine();

if (str.equals("END"))

break;

System.out.println("Echoing: "+
str);

out.println(str);

}

// Always close the two sockets...

} finally {

System.out.println("closing...");

socket.close();

}

} finally {

s.close();

}

}

} /// :~

ServerSocket 需要的只是一个端口编号,不需要IP 地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行。建好一个连接以后,accept()就会返回一个Socket对象,它是那个连接的代表。

清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket 构建器失败,则程序简单地退出(注意必须保证ServerSocket的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“掷”出一个IOException 违例,所以不必使用一个try 块。若ServerSocket 构建器成功执行,则其他所有方法调用都必须到一个try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket
都能正确地关闭。

同样的道理也适用于由accept()返回的Socket。若accept()失败,那么我们必须保证Socket 不再存在或者含有任何资源,以便不必清除它们。但假若执行成功,则后续的语句必须进入一个try-finally 块内,以保障在它们失败的情况下,Socket 仍能得到正确的清除。由于套接字使用了重要的非内存资源,所以在这里必须特别谨慎,必须自己动手将它们清除(Java 中没有提供“破坏器”来帮助我们做这件事情)。

无论ServerSocket 还是由accept()产生的Socket 都打印到System.out 里。这意味着它们的toString方法会得到自动调用。这样便产生了:

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]

Socket[addr=127.0.0.1,PORT=1077,localport=8080]

程序的下一部分看来似乎仅仅是打开文件,以便读取和写入,只是InputStream 和OutputStream 是从Socket 对象创建的。利用两个“转换器”类InputStreamReader 和OutputStreamWriter ,InputStream 和OutputStream 对象已经分别转换成为Java 1.1 的Reader 和Writer 对象。也可以直接使用Java1.0 的InputStream 和OutputStream
类,但对输出来说,使用Writer 方式具有明显的优势。这一优势是通过PrintWriter 表现出来的,它有一个过载的构建器,能获取第二个参数——一个布尔值标志,指向是否在每一次println()结束的时候自动刷新输出(但不适用于print()语句)。每次写入了输出内容后(写进out),它的缓冲区必须刷新,使信息能正式通过网络传递出去。对目前这个例子来说,刷新显得尤为重要,因为客户和服务器在采取下一步操作之前都要等待一行文本内容的到达。若刷新没有发生,那么信息不会进入网络,除非缓冲区满(溢出),这会为本例带来许多问题。

编写网络应用程序时,需要特别注意自动刷新机制的使用。每次刷新缓冲区时,必须创建和发出一个数据包(数据封)。就目前的情况来说,这正是我们所希望的,因为假如包内包含了还没有发出的文本行,服务器和客户机之间的相互“握手”就会停止。换句话说,一行的末尾就是一条消息的末尾。但在其他许多情况下,消息并不是用行分隔的,所以不如不用自动刷新机制,而用内建的缓冲区判决机制来决定何时发送一个数据包。这样一来,我们可以发出较大的数据包,而且处理进程也能加快。

注意和我们打开的几乎所有数据流一样,它们都要进行缓冲处理。

无限while 循环从BufferedReaderin 内读取文本行,并将信息写入System.out,然后写入PrintWriter.out。注意这可以是任何数据流,它们只是在表面上同网络连接。

客户程序发出包含了"END"的行后,程序会中止循环,并关闭Socket。

下面是客户程序的源码:

1.2     客户端

import java.net.*;

import java.io.*;

public
class
JabberClient {

public
staticvoid
main(String[]
args)throwsIOException {

// Passing null to getByName() produces the

// special "Local
Loopback" IP address, for

// testing on one machine w/o a network:

InetAddress addr = InetAddress.getByName(null);

// Alternatively, you can use

// the address or name:

// InetAddress
addr =

// InetAddress.getByName("127.0.0.1");

// InetAddress
addr =

// InetAddress.getByName("localhost");

System.out.println("addr = " +
addr);

Socket socket =
new Socket(addr, JabberServer.PORT);

// Guard everything in a try-finally to make

// sure that the socket is closed:

try {

System.out.println("socket = "+
socket);

BufferedReader
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));

// Output is automatically flushed

// by PrintWriter:

PrintWriter
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())),

true);

for (int
i = 0; i < 10;
i++) {

out.println("howdy " +
i);

String
str = in.readLine();

System.out.println(str);

}

out.println("END");

} finally {

System.out.println("closing...");

socket.close();

}

}

} /// :~

在main()中,可看到获得本地主机IP 地址的InetAddress 的三种途径:使用null,使用localhost,或者直接使用保留地址127.0.0.1。当然,如果想通过网络同一台远程主机连接,也可以换用那台机器的IP地址。打印出InetAddress addr 后(通过对toString()方法的自动调用),结果如下:

localhost/127.0.0.1

通过向getByName()传递一个null,它会默认寻找localhost,并生成特殊的保留地址127.0.0.1。注意在名为socket 的套接字创建时,同时使用了InetAddress 以及端口号。打印这样的某个Socket 对象时,为了真正理解它的含义,请记住一次独一无二的因特网连接是用下述四种数据标识的:clientHost(客户主机)、clientPortNumber(客户端口号)、serverHost(服务主机)以及serverPortNumber(服务端口号)。服务程序启动后,会在本地主机(127.0.0.1)上建立为它分配的端口(8080)。一旦客户程序发出请求,机器上下一个可用的端口就会分配给它(这种情况下是1077),这一行动也在与服务程序相同的机器(127.0.0.1)上进行。现在,为了使数据能在客户及服务程序之间来回传送,每一端都需要知道把数据发到哪里。所以在同一个“已知”服务程序连接的时候,客户会发出一个“返回地址”,使服务器程序知道将自己的数据发到哪儿。我们在服务器端的示范输出中可以体会到这一情况:

Socket[addr=127.0.0.1,port=1077,localport=8080]

这意味着服务器刚才已接受了来自127.0.0.1 这台机器的端口1077 的连接,同时监听自己的本地端口(8080)。而在客户端:

Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]

这意味着客户已用自己的本地端口1077 与127.0.0.1 机器上的端口8080 建立了 连接。注意到每次重新启动客户程序的时候,本地端口的编号都会增加。这个编号从1025(刚好在系统保留的1-1024 之外)开始,并会一直增加下去,除非我们重启机器。若重新启动机器,端口号仍然会从1025 开始增值(在Unix 机器中,一旦超过保留的套按字范围,数字就会再次从最小的可用数字开始)。

创建好Socket 对象后,将其转换成BufferedReader 和PrintWriter 的过程便与在服务器中相同(同样地,两种情况下都要从一个Socket 开始)。在这里,客户通过发出字串"howdy",并在后面跟随一个数字,从而初始化通信。注意缓冲区必须再次刷新(这是自动发生的,通过传递给PrintWriter 构建器的第二个参数)。若缓冲区没有刷新,那么整个会话(通信)都会被挂起,因为用于初始化的“howdy”永远不会发送出去(缓冲区不够满,不足以造成发送动作的自动进行)。从服务器返回的每一行都会写入System.out,以验证一切都在正常运转。为中止会话,需要发出一个"END"。若客户程序简单地挂起,那么服务器会“掷”出一个违例。

大家在这里可以看到我们采用了同样的措施来确保由Socket 代表的网络资源得到正确的清除,这是用一个try-finally 块实现的。

套接字建立了一个“专用”连接,它会一直持续到明确断开连接为止(专用连接也可能间接性地断开,前提是某一端或者中间的某条链路出现故障而崩溃)。这意味着参与连接的双方都被锁定在通信中,而且无论是否有数据传递,连接都会连续处于开放状态。从表面看,这似乎是一种合理的连网方式。然而,它也为网络带来了额外的开销。本章后面会介绍进行连网的另一种方式。采用那种方式,连接的建立只是暂时的。

时间: 2024-11-05 14:59:21

67.JAVA编程思想——套接字的相关文章

66.JAVA编程思想——网络编程

66.JAVA编程思想--网络编程 历史上的网络编程都倾向于困难.复杂,而且极易出错. 程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识.一般地,我们需要理解连网协议中不同的"层"(Layer).而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接.打包和拆包:这些块的来回运输:以及握手等等.这是一项令人痛苦的工作.但是,连网本身的概念并不是很难.我们想获得位于其他地方某台机器上的信息,并把它们移到这儿:或者相反.这与读写文件非常相似,只是文件存在于远程

39.JAVA编程思想之外篇——JAVA图形化设计精简大全一文覆盖

39.JAVA编程思想之外篇--JAVA图形化设计精简大全一文覆盖 欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51204948 目录 Java图形化界面设计--容器(JFrame)...1 Java基本类(JFC)...1 l     AWTAbstract Window Toolkit(AWT)抽象窗口工具包... 2 l     Swing包... 2 l     AWT和Swing的区别... 6 Swing基本框

70.JAVA编程思想——Web应用

70.JAVA编程思想--Web应用 创建一个应用,令其在真实的Web 环境中运行,它将把Java 的优势表现得淋漓尽致.这个应用的一部分是在Web 服务器上运行的一个Java 程序,另一部分则是一个"程序片"或"小应用程序"(Applet),从服务器下载至浏览器(即"客户").这个程序片从用户那里收集信息,并将其传回Web 服务器上运行的应用程序.程序的任务非常简单:程序片会询问用户的E-mail 地址,并在验证这个地址合格后(没有包含空格,而

69.JAVA编程思想——数据报

69.JAVA编程思想--数据报 迄今看到的例子使用的都是"传输控制协议"(TCP),亦称作"基于数据流的套接字".根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地.换言之,它允许重传那些由于各种原因半路"走失"的数据.而且收到字节的顺序与它们发出来时是一样的.当然,这种控制与可靠性需要我们付出一些代价:TCP 具有非常高的开销. 还有另一种协议,名为"用户数据报协议"(UDP),它并不刻意追求数据包会完

68.JAVA编程思想——服务多个客户

68.JAVA编程思想--服务多个客户 JabberServer 可以正常工作,但每次只能为一个客户程序提供服务.在典型的服务器中,我们希望同时能处理多个客户的请求.解决这个问题的关键就是多线程处理机制.而对于那些本身不支持多线程的语言,达到这个要求无疑是异常困难的.Java 已对多线程的处理进行了尽可能的简化.由于Java 的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事.最基本的方法是在服务器(程序)里创建单个ServerSocket,并调用accept()来等候一个新连接.一旦

12.JAVA编程思想——集合的类型

12.JAVA编程思想--集合的类型 欢迎转载,转载请标明出处:http://blog.csdn.net/notbaron/article/details/51100510 标准Java 1.0 和1.1 库配套提供了非常少的一系列集合类.但对于自己的大多数编程要求,它们基本上都能胜任.Java 1.2 提供的是一套重新设计过的大型集合库. 1      Vector Vector 的用法很简单,大多数时候只需用addElement()插入对象,用elementAt()一次提取一个对象,并用el

1.JAVA 编程思想——对象入门

对象入门 欢迎转载,转载请标明出处:    http://blog.csdn.net/notbaron/article/details/51040219 如果学JAVA,没有读透<JAVA 编程思想>这本书,实在不好意思和别人说自己学过JAVA.鉴于此,蛤蟆忙里偷闲,偷偷翻看这本传说中的牛书. 面向对象编程OOP具有多方面吸引力.实现了更快和更廉价的开发与维护过程.对分析与设计人员,建模处理变得更加简单,能生成清晰.已于维护的设计方案. 这些描述看上去非常吸引人的,不过蛤蟆还是没啥印象(至少到

71.JAVA编程思想——JAVA与CGI

71.JAVA编程思想--JAVA与CGI Java 程序可向一个服务器发出一个CGI 请求,这与HTML 表单页没什么两样.而且和HTML 页一样,这个请求既可以设为GET(下载),亦可设为POST(上传).除此以外,Java 程序还可拦截CGI 程序的输出,所以不必依赖程序来格式化一个新页,也不必在出错的时候强迫用户从一个页回转到另一个页.事实上,程序的外观可以做得跟以前的版本别无二致. 代码也要简单一些,毕竟用CGI 也不是很难就能写出来(前提是真正地理解它).所以我们准备办个CGI 编程

Java编程思想第四版读书笔记——第十三章 字符串

Java编程思想第四版读书笔记--第十三章 字符串 字符串的操作是计算机程序设计中最常见的行为. 关键词: StringBuilder ,StringBuffer,toString(),format转换,正则表达式, 1.不可变String String对象时不可变的.每当把String对象作为方法的参数时,都会复制一份引用.(其实就是对函数中参数列表中参数的操作不会影响外面的原参数) 如下: import static net.mindview.util.Print.*; public cla