网络编程往往都是困难、复杂,而且极易出错的。程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解网络协议中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者发送的数据。
Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏在JVM 以及Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接(一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便的。
当然,为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但Java面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了IP(互联网地址)的概念。
public static void main(String[] args) throws IOException {
//认识本机的IP地址
InetAddress ia = InetAddress.getLocalHost();
System.out.println(ia);
System.out.println(ia.getHostName());
System.out.println(ia.getHostAddress());
byte[] buf = ia.getAddress();
System.out.println(buf.length);
System.out.println(Arrays.toString(buf));
//认识百度的IP地址
InetAddress ia2 = InetAddress.getByName("www.baidu.com");
System.out.println(ia2);
System.out.println(ia2.getHostAddress());
System.out.println(Arrays.toString(ia2.getAddress()));
InetAddress addr = InetAddress.getByName("www.baidu.com");
int port = 4321;
InetSocketAddress isa = new InetSocketAddress(addr, port);
System.out.println(isa);
System.out.println(isa.getAddress());
System.out.println(isa.getPort());
}
网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对方,就可以展开一次令人愉快的双向对话。服务器和客户机它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务器端还是客户机端,连接只是魔术般地变成了一个 IO数据流对象。从这时开始,我们可以象读写一个普通的文件那样对待连接。所以一旦建好连接,我们只需使用自己熟悉的 IO 命令即可。这正是Java连网最方便的一个地方。
public class ClientTest {
public static void main(String[] args) throws IOException {
//localhost 表示 127.0.0.1 也就是主机自身 8080表示端口号 一般的web项目默认端口号是8080
Socket socket = new Socket(InetAddress.getByName("localhost"),8080);
//向服务器端发出一个请求
OutputStream outputStream = socket.getOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
dataOutputStream.writeUTF("客户端给服务器端发送了一个请求");
//释放资源
outputStream.close();
dataOutputStream.close();
}
}
public class ServerTest {
public static void main(String[] args) throws IOException {
System.out.println("----------------服务器已启动----------------");
//监听客户端
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
//接受客户端发来的请求
InputStream inputStream = socket.getInputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
//打印结果
String result = dataInputStream.readUTF();
System.out.println("服务器端收到了客户端发来的请求 : " + result);
dataInputStream.close();
inputStream.close();
}
}
有些时候,一个IP地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服务器(程序)。由IP 表达的每台机器也包含了“端口”(Port)。我们设置一个客户机或者服务器的时候,必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,IP 地址是他居住的房子,而端口是他在的那个房间。注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知道如何通过机器的IP 地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着一种服务,一台机器可能提供了多种服务,比如HTTP 和 FTP等等)?端口编号在这里扮演了重要的角色,它是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。系统服务保留了使用端口1 到端口 1024 的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在使用的端口。
服务器的全部工作就是等候建立一个连接,然后用那个连接产生的Socket创建一个 InputStream 以及一个OutputStream。在这之后,它从InputStream读入的所有东西都会反馈给OutputStream,直到接收到行中止(END)为止,最后关闭连接。客户机连接与服务器的连接,然后创建一个OutputStream。文本行通过 OutputStream发送。客户机也会创建一个InputStream,用它收听服务器说些什么(本例只不过是反馈回来的同样的字句)。服务器与客户机(程序)都使用同样的端口号,而且客户机利用本地主机地址连接位于同一台机器中的服务器(程序),所以不必在一个物理性的网络里完成测试(在某些配置环境中,可能需要同真正的网络建立连接,否则程序不能工作——尽管实际并不通过那个网络通信)
ServerSocket需要的只是一个端口编号,不需要 IP地址(因为它就在这台机器上运行)。调用accept()时,方法会暂时陷入停顿状态(堵塞),直到某个客户尝试同它建立连接。换言之,尽管它在那里等候连接,但其他进程仍能正常运行(参考第 14章)。建好一个连接以后,accept()就会返回一个 Socket 对象,它是那个连接的代表。清除套接字的责任在这里得到了很艺术的处理。假如ServerSocket 构建器失败,则程序简单地退出(注意必须保证ServerSocket 的构建器在失败之后不会留下任何打开的网络套接字)。针对这种情况,main()会“掷”出一个IOException 违例,所以不必使用一个try块。若 ServerSocket构建器成功执行,则其他所有方法调用都必须到一个 try-finally代码块里寻求保护,以确保无论块以什么方式留下,ServerSocket都能正确地关闭。
public class Person implements Serializable {
//账号
private String son;
//密码
private String password;
}
public class ClientTest {
public static void main(String[] args) throws IOException {
//localhost 表示 127.0.0.1 也就是主机自身 8080表示端口号 一般的web项目默认端口号是8080
Socket socket = new Socket(InetAddress.getByName("localhost"),8080);
//向服务器端发出一个请求
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
//使用对象流
ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);
//获得用户输入的结果
Person person = new Person("369613719","123456");
//将对象发送过去
objectOutputStream.writeObject(person);
//接受服务器发来的结果
DataInputStream dataInputStream = new DataInputStream(inputStream);
System.out.println(dataInputStream.readUTF());
//释放资源
outputStream.close();
objectOutputStream.close();
}
}
public class ServerTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
System.out.println("----------------服务器已启动----------------");
//监听客户端
ServerSocket serverSocket = new ServerSocket(8080);
Socket socket = serverSocket.accept();
//创建流用来数据传输
OutputStream outputStream = socket.getOutputStream();
InputStream inputStream = socket.getInputStream();
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
//接受客户端发来的请求
Person person = (Person) objectInputStream.readObject();
String result = "";
//判断是否登陆成功
if("369613719".equals(person.getSon()) && "123456".equals(person.getPassword())){
result = "登陆成功";
}else{
result = "登录失败";
}
//发送结果
dataOutputStream.writeUTF(result);
//释放资源
inputStream.close();
outputStream.close();
objectInputStream.close();
dataOutputStream.close();
}
}
使用ObjectInputStream与ObjectOutputStream来实现对象的传递时需要注意的是。如果client端的源程序与server端的源程序位于两个项目中,那么需要传递对象的类的包名必须一样,否则会抛出:java.lang.ClassNotFoundException的异常。