Java 和 客户端/服务器
客户端-服务器应用程序无处不在。客户端-服务器可以定义为一种为其他进程提供服务的进程。客户端和服务器可以在同一台机器上运行,也可以在世界两端的不同机器上运行。一个非编程的客户端-服务器情况的例子是电话系统。您是客户端(或客户),而中心局是服务器。通过将电话连接到系统(并且您的账单是最新的!),您正在订阅中心局提供的服务。向服务器(中心局)发出请求以拨打和接听电话。服务器还对每次拨打或接听的电话进行计费,并处理紧急(911)请求。在本文中,我将介绍一个简单的 CB(公民波段)无线电模拟器,它是为一个课堂项目编写的。服务器是用 C 语言编写的,客户端是用 Java 编写的。我将假设读者了解什么是套接字,并且对它们的使用方法有一个大致的了解。
该项目的规范大致如下
提供一个可以接受多个并发连接的服务器。服务器应该有一个基本的命令集,可以打开和关闭连接、更改频道,并提供特定频道上当前客户端的列表。
提供一个“紧急频道”(9),它将所有流量广播到所有当前连接的客户端,无论他们订阅的是哪个频道。
客户端应在频道 19 上“启动”。这是 CB 用户会面的频道。然后他们商定要移动到哪个频道以继续他们的对话。
客户端应显示当前频道上的所有流量,并显示发送消息的人的句柄。
服务器可以是单进程并发服务器或多进程服务器(稍后详细介绍)。
除了因为做图形用户界面而获得额外学分外,我选择 Java 是为了简化客户端的编程。选择 Java 的具体原因如下
可移植性。我在家里的 Linux 系统上开发代码,然后在学校的 Suns 工作站上运行代码。
功能性。Java 不仅仅用于网页动画,它也是一种非常有用的编程语言。Java 是面向对象的,并且非常类似于 C 和 C++。编写一个简单的用户界面相对容易。
线程。Java 允许执行多个线程(如后台进程)。可以启动一个线程来监听传入的消息。当消息传入时,它会自动发送到输出。我们启动这个线程然后就不用管它了。
可以从 Netscape(或类似的浏览器)远程运行。虽然我目前尚未实现此功能,但可以想象,浏览您网页的网民可以使用 CB 模拟器相互交谈。对此有很多限制,我们稍后将详细介绍。
对于客户端使用 C 语言需要更多的编程才能达到相同的结果。首先,需要使用某种 GUI 构建器,例如 Motif 或 X-Forms。我并不是要贬低它们中的任何一个,但并非每个系统都具有它们,并且它们可能难以学习和使用。C 语言没有线程,因此所有 I/O 都必须轮询。用户输入以及传入的消息都必须轮询并进行相应的处理。如果没有 GUI,则必须开发某种命令代码供用户控制客户端和服务器。这可能会非常晦涩难懂且难以使用——更不用说难以实现了。
我首先根据 表 1 中的规范开发了服务器。消息是固定长度的,并且不得与给定的格式有所不同。C 语言通过使用结构和指针很好地处理传输;基本上,您只需调用写入或读取例程,将指向数据结构的指针传递给它,字节就会来来去去,没有太多问题。这对于 C 语言来说很好;Java 则是另一回事。
套接字在 Java 中的工作方式与它们在 C 语言中的对应物几乎相同。由于 Java 是面向对象的,因此您必须创建套接字对象的实例。这可以通过简单的代码行来完成
Socket s = new Socket(hostName,portNumber);
其中 s 是 Socket 类型的实例,而 hostName,portNumber 是要连接的主机和端口的名称。但是,如果没有数据输入流和数据输出流,套接字本身并不是很有用。下面的代码段设置了一个数据输入流和数据输出流来与套接字通信
dis= new DataInputStream(s.getInputStream()); dos= new DataOutputStream(new BufferedOutputStream(s.getOutputStream()));
输出流创建为缓冲输出流。除非 Java 认为有足够的数据要写入,否则数据不会写入套接字,或者您强制写入——通过使用类似 dos.flush() 的方式进行刷新;这会在数据输出流上调用 flush 方法。在套接字的读取端,我们可以简单地进入一个无限循环并轮询来自服务器的数据,因为监听器作为单独的线程运行。
Java 具有与 C 语言基本相同的大多数基本数据类型,但有一些例外。Java 没有无符号整数,但它有真正的布尔值。要构造数据包,请使用 Integer 和一个 120 字节的数组的组合来表示句柄和消息字段。数据输出流和输入流具有用于读取和写入整数和字节的方法。例如,dos.writeInt(1); 会将整数“1”写入数据输出流。相反,for (int i=0;i<120;i++) dos.writeByte(buffer[i]); (或 dos.readByte(buffer[i]) 用于读取)会将整个缓冲区写入套接字;dos.flush() 将确保数据现在写入而不是延迟写入。重要的是要注意,即使我们只想更改频道,我们也必须将所有数据(命令、频道、句柄和消息)写入或读取到服务器或从服务器读取。服务器期望这样做;否则它将挂起,等待所有字节的到来或离开。
还有一个障碍仍然存在。如何将句柄和消息数据放入字节数组中的正确位置?在事件处理程序中,我们为消息和句柄创建字符串对象,然后对字符串对象调用 getBytes() 方法。message.getBytes(0,message.length(),buffer,20); 会将从字符串对象 message 中位置 0 开始的 message.length() 个字节复制到字节数组 buffer 中,从位置 20 开始。我的程序中缺少的一件事是错误检查。在生产程序中,绝对有必要检查和重新检查以确保您不会因写入超过缓冲区可以容纳的字节数而导致缓冲区溢出。
服务器是一个简单的单进程并发服务器。简单来说,服务器轮询每个连接,并按顺序处理请求。另一种选择是为每个传入的连接 fork 一个新进程。在这种情况下,单进程服务器要简单得多(而且,毕竟,计算机一次只能真正做一件事)。基本流程是
在众所周知的端口上创建主套接字。
绑定套接字,以便将传入的请求定向到正确的位置。
监听连接。
接受传入的连接。
“众所周知的端口”是所有客户端都知道的端口。所有客户端都无法连接到同一端口,因此服务器将连接请求“移交”到另一个端口。这是由 TCP/IP 层完成的,我们不需要关心它是如何完成的。这个过程类似于拨打大型公司的免费电话号码。假设您想拨打 1-800-257-1234。您正在请求服务器在该端口(电话)号码上建立连接。这家公司可能有数百条线路,但您不会想尝试每一条线路直到最终接通,因此该公司在其线路上设置了一个旋转开关,以便将连接转接到下一条可用的电话线。
TCP/IP 套接字的工作方式相同。当在套接字上接受连接时,会创建一个新的文件描述符。文件描述符用作结构数组的索引。每个客户端都恰好有一个唯一的文件描述符和客户端数组中的一个槽位。每个数组位置都保存一个结构,该结构包含句柄和当前频道号。当服务器收到消息包时,它会遍历整个数组,并将消息重新传输给所有订阅了消息来源频道号的客户端。
当前支持的服务器命令有
CB_ON 将客户端的频道设置为 19,并发送欢迎消息。它还将句柄存储在客户端数组中。
CB_OFF 关闭套接字并从客户端数组中清除客户端信息。
SET_CHAN 更改客户端数组中客户端的频道。
WHO_CHAN 发送一条消息,其中包含订阅当前频道的所有已连接客户端的句柄。
SEND-MESSAGE 将数据包消息字段中包含的消息发送给每个订阅与发起者相同频道的客户端。频道 9 上的紧急流量也会发送给所有已连接的客户端,无论他们订阅的是哪个频道。如前所述,服务器必须一次性发送或接收数据包中的所有字节。在此实现中,不可能发送数据包的一部分。
我的最初目标是制作一个看起来像 CB 无线电面板的客户端。事实证明,使用 Java 很难做到这一点;虽然 Java 是一种很好的可移植编程语言,但创建复杂的用户界面非常困难。我从 David Flanagan 的 Java in a Nutshell(O'Reilly & Associates 出版,一本很棒的书——非常适合参考)中的一个示例改编了我的客户端。CB 客户端用户界面非常简单。顶部有一个“连接”菜单。从这里,用户可以退出或询问服务器当前频道上有哪些用户。中间窗口是消息区域。此处列出了来自其他用户和服务器的所有消息。客户端将打印来自数据包的句柄和消息。服务器负责这些数据包中的数据,因为它会在 WHO 请求的句柄中放入“System: WHO”。底部字段用于输入新频道。当 Java 检测到菜单栏或频道字段中的活动时,它将调用事件处理程序例程。从这里,它确定事件的来源并执行适当的处理。用户界面并不怎么样——更像是一个“概念验证”而不是其他——但它确实在更少的代码行中提供了比用 C 语言编写的等效程序所需的功能多得多的功能。
大端与小端的争论一直是互联网上许多口水战的主题。但它到底是什么?大端和小端指的是字节的顺序。在移动数据时,某些系统从最高有效字节 (MSB) 开始,而某些系统从最低有效字节 (LSB) 开始。想象一个 4 字节的数组。您如何存储或发送这个数组?您会从 LSB(小端)开始还是从 MSB(大端)开始?有些硬件以一种方式执行,而有些硬件以另一种方式执行。我们为什么要关心?如果您正在用 C 语言编写客户端和服务器以在相同类型的硬件上运行,则不会出现字节序问题。但是,如果您使用不同的语言(例如 Java)与用 C 语言编写的服务器通信,则可能会出现大问题。仅当通过网络发送多字节数据类型(如整数)时,字节序问题才会出现。当 Java 通过套接字发送数据时,它会自动将其数据转换为网络字节序和从网络字节序转换。另一方面,C 语言只按指令执行。有两个 C 系统调用 ntohl() 和 htonl(),它们将数据转换为网络字节序和从网络字节序转换。阅读这些调用的手册页并在您基于 C 语言的服务器和客户端中使用它们,以避免字节序问题。
Java 有一些严格的安全限制。小程序只能打开到加载它的服务器的套接字。另一方面,应用程序可以打开到任何机器的套接字。我的客户端是为此原因编写为独立应用程序的。(我没有访问权限的 Web 服务器,该服务器允许我运行我的 CB 服务器。)小程序和应用程序之间几乎没有主要区别。小程序扩展了 applet 类,应用程序扩展了 frame 类。有关更多具体细节,请参阅 Java 书籍。
这个项目是我第一次真正尝试客户端-服务器编程。我被迷住了!在编写了基本服务器之后,可以扩展代码来做很多事情。我最终希望重新设计用户界面,使其看起来更好并且更易于使用。在家中使用 Linux 使程序开发过程变得容易得多。我能够在我的家用系统和学校的 Sun 工作站上使用相同的工具,因此只需简单地重新编译一下,服务器就可以在 Sparc 5 上运行。我希望其他人会发现这项工作有用。在任何 Java 书籍(我有三本)中都找不到针对此特定应用程序的参考文献。虽然所有这些书籍中都有客户端-服务器应用程序,但所有服务器都是用 Java 编写的。Java 非常适合编写服务器,但速度不如 C 语言快,并且需要更多的系统资源才能运行。每种语言都有其位置,Java 也不例外。Java 作为客户端编程语言非常有用;它将继续存在。
查看列表
Java in a Nutshell
David Flanagan (1996, O'Reilly & Associates, Inc)
Java Programming Explorer
Neil Bartlett, Alex Leslie, 和 Steve Simkin (1996, Coriolis Group Books)
Teach Yourself Java in 21 Days
Lemay 和 Perkins (1996 Sams.net publishing)
Internetworking with TCP/IP vol III
Comer 和 Stevens (1993, Prentice-Hall, Inc)
The C Programming Language,第二版
Kernighan 和 Ritchie (1988, 1978, Prentice Hall)
各种 Linux 手册页
Joe Novosel (jnovosel@cc.gatech.edu) 自 1981 年以来一直是一位狂热的计算机爱好者,当时他的第一台计算机 (Radio Shack Color Computer) 拥有惊人的 4K 内存(包括显存!)。他使用 Linux 大约两年了——自 1.1.47 版本以来——并且认为 Linux 带回了他早期计算机时代的兴奋。在电气行业工作几年后,Joe 决定重返校园,现在是佐治亚理工学院的一名大三学生,在那里他攻读计算机科学学位。