Java网络编程技术

网络编程对于很多的初学者来说,是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编程的大门而放弃了对于该部分技术的学习。

在学习网络编程以前,很多初学者可能觉得网络编程是比较复杂的系统工程,需要了解很多和网络相关的基础知识,其实这些都不是很必需的。首先来问一个问题:你会打手机吗?很多人可能说肯定会啊,不就是按按电话号码,拨打电话嘛,很简单的事情啊!其实初学者如果入门网络编程的话也可以做到这么简单!

网络编程就是在两个或两个以上的设备(例如计算机)之间传输数据。程序员所作的事情就是把数据发送到指定的位置,或者接收到指定的数据,这个就是狭义的网络编程范畴。在发送和接收数据时,大部分的程序设计语言都设计了专门的API实现这些功能,程序员只需要调用即可。所以,基础的网络编程可以和打电话一样简单。

计算机网络概述

通过一定的物理设备将处于不同位置的计算机连接起来就组成了计算机网络。这个网络中可能包含的设备有:计算机、路由器、交换机等。

网络最主要的优势在于共享:共享设备和数据。现在共享设备最常见的是打印机的共享,一个公司一般拥有一个打印机就可以了。

从软件角度来说,对于物理设备的理解不需要很深刻,就像我们打电话时不需要弄清楚通信网络的底层实现是一样的,但是当深入到网络编程的底层时,这些基础知识还是必须要具备的。当然如果想要了解更多的网络底层知识,我们可以查阅相关技术资料文档,在此先不作具体讲解。

对于网络编程来说,最主要的是计算机之间的通信,这样首要的问题就是解决如何找到网络上的计算机的问题。那我们首先来需要了解IP地址的概念。

IP地址的作用是为了唯一地标识网络中的每一个设备。现在使用最广的IP地址协议仍然是IPv4,关于IPv6大家可以自己查询相关文档。IPv4是以点分十进制法记忆的,实际上是以32位二进制来唯一标识设备,每8位为一段,然后将每段的二进制值转换为对应的十进制值,以小圆点(.)分隔,如:10.22.135.79,所以每段的取值都介于0-255之间。

每个接入网络的计算机都拥有唯一的IP地址,但由于IP地址记忆不变,所以又提出了域名(Domain Name)的概念。域名是使用简洁的有意义的字符代替IP地址的方式,一个IP可以对应多个域名,但一个域名只能对应一个IP地址。如果说IP地址类似于手机号,那么域名就相当于是手机中的电话簿。由于手机号码不方便记忆,我们添加一个姓名标识该号码,在实际拨打电话时可以选择该姓名,然后拨打即可。

那如何将一个域名和一个IP地址映射起来呢,这就需要DNS服务器。例如当用户在浏览器输入域名时,浏览器首先请求DNS服务器,将域名解析为IP地址,然后将解析后的IP地址反馈给浏览器,再进行实际的数据传输。

IP地址和域名很好的解决了在网络中找到一个计算机的问题,但是为了让一个计算机可以同时运行多个网络程序,就引入了另外一个概念——端口(port)。这就好比一个公司前台有一个电话,每个员工有一个分机,这样如果需要找到这个员工的话,需要首先拨打前台总机,然后转该分机号即可。这样既减少了公司的开销,也方便了每个员工。在该示例中前台总机的电话号码就相当于IP地址,而每个员工的分机号就相当于端口。

有了端口的概念以后,在同一个计算机中每个程序对应唯一的端口,这样一个计算机上就可以通过端口区分发送给每个端口的数据了,换句话说,也就是一个计算机上可以并发运行多个网络程序,而不会在互相之间产生干扰。

在硬件上规定,端口的号码必须位于0-65535之间,每个端口唯一的对应一个网络程序,一个网络程序可以使用多个端口。这样一个网络程序运行在一台计算上时,不管是客户端还是服务器,都是至少占用一个端口进行网络通讯。在接收数据时,首先发送给对应的计算机,然后计算机根据端口把数据转发给对应的程序。

网络编程概述

我们以一个例子来说明网络通讯模型。

假如我们去面试,一般会是这样的:

面试官:请问你的爱好是什么呢?

面试者:吃饭睡觉打豆豆

面试官:那你能接受每周工作七天,每天工作12个小时吗?

面试者:大概可以吧

……

这个例子中,面试官问一句,面试者答一句,我们假定,如果面试官不问,面试者就保持沉默。这种一问一答的形式就是网络通讯中的“请求-响应”模型。

网络通讯中,等待连接并提供服务的一端通常称为服务器端(Server),而发起连接并使用服务的一端称为客户端(Client)。当然,一旦通讯建立了,客户端和服务器端完全一样,没有本质的区别。

我们经常使用的QQ,每个用户都会安装一个QQ程序,当然用户安装的是客户端程序,而服务器端则运行在QQ的电信机房中为广大QQ用户服务。在网络编程中,这种结构被称作是C/S结构,即客户端/服务器结构。

使用C/S结构的程序,因为需要独立安装客户端程序才能访问服务器端资源,所以这种模式数据的共享是比较局部的,对于数据安全性要求较高,或是用户体验要求较强的程序来说比较适用,但同时如果我们需要维护这类系统,维护代价也是比较高的。比如我们要做QQ升级,每个客户端都得重新安装新的程序。

我们另外还有一种结构就是B/S结构,即浏览器/服务器结构。这种结构我们只要重点开发服务器端即可,没有必须使用专用的客户端, 只需要客户端安装一个浏览器即可。它的优势在于开发压力比较小,数据共享范围广,不需要维护客户端。但也有很多不足,比如浏览器表现力不足,用户体验不强等。

最后再介绍一个网络编程中最重要,也是最复杂的概念——协议(Protocol)。按照前面的介绍,网络编程就是运行在不同计算机中两个程序之间的数据交换。在实际进行数据交换时,为了让接收端理解该数据,就需要规定该数据的格式,这个数据的格式就是协议。

比如我们上课要给喜欢的女孩子传纸条,但是又不想让老师和其他同学知道内容,那么我们可以私下约定一些暗语。我们将要传递的内容转换成暗语传递出去,而接收方收到的是转换之后的内容,如果需要知道具体表述意思,则只需按照约定的暗语翻译出来即可。没有暗语对应的明文意思,我们就无法翻译。那么这里的暗语约定就是一种协议。

那么如何来编写协议格式呢?答案是随意。只要按照这种协议格式能够生成唯一的编码,按照该编码可以唯一的解析出发送数据的内容即可。也正因为各个网络程序之间协议格式的不同,所以才导致了客户端程序都是专用的结构。

在实际的网络程序编程中,最麻烦的内容不是数据的发送和接收,因为这个功能在几乎所有的程序语言中都提供了封装好的API进行调用,最麻烦的内容就是协议的设计以及协议的生产和解析,这个才是网络编程中最核心的内容。

网络通讯方式

在现有的网络中,网络通讯的方式主要有两种:

  • TCP(传输控制协议)方式
  • UDP(用户数据报协议)方式

再举个例子,我们使用手机联系别人也可以有两种方式:打电话和发短信。打电话可以保证将信息传递给别人,因为别人接听电话就确认接收到了该信息,这就类似网络通讯中的TCP方式。发短信价格低廉,使用方便,但是不能保证信息的接收人一定能接收到信息,这就是类似内乡通讯中的UDP方式。

使用TCP协议进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而使用UDP协议方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败客户端无法获得。

这两种方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。

由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。

网络编程步骤

客户端网络编程步骤

客户端的编程主要由三个步骤实现:

  1. 建立网络连接:客户端网络编程的第一步都是建立网络连接。在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。

  2. 交换数据:连接建立以后,就可以通过这个连接交换数据了。交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。

  3. 关闭网络连接:在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。

服务器端网络编程步骤

服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:

  1. 监听端口:服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。

  2. 获得连接:当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。

  3. 交换数据:服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。当然,服务器端的数据交换也是可以多次进行的。在数据交换完成以后,关闭和客户端的连接。

  4. 关闭连接:当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。

Java网络编程

Java中和网络编程有关的API位于java.net包中,该包中包含了基本的网络编程实现,该包是网络编程的基础。

首先来介绍一个基础的网络类——InetAddress类。该类的功能是代表一个IP地址,并且将IP地址和域名相关的操作方法包含在该类的内部。

在Java中分别用Inet4Address和Inet6Address类来描述IPv4和IPv6的地址。这两个类都是InetAddress的子类。由于InetAddress没有public的构造方法,因此,要想创建InetAddress对象,必须得依靠它的四个静态方法。InetAddress可以通过getLocalHost方法得到本机的InetAddress对象,也可以通过getByName、getAllByName和getByAddress得到远程主机的InetAddress对象。

getLocalHost()方法

使用getLocalHost可以得到描述本机IP的InetAddress对象。这个方法的定义如下:

public static InetAddress getLocalHost() throws UnknownHostException

这个方法抛出了一个UnknownHostException异常,因此,必须在调用这个方法的程序中捕捉或抛出这个异常。下面的代码演示了如何使用getLocalHost来得到本机的IP和计算机名。

import java.net.InetAddress;

/**
 * InetAddres测试类
 * 
 * @author 小明
 *
 */
public class MyInetAddress {
    public static void main(String[] args) throws Exception {
        // 得到描述本机IP的InetAddress对象
        InetAddress localAddress = InetAddress.getLocalHost();
        // 打印
        System.out.println(localAddress);
    }
}

运行结果:

NNOYHM42DZMMTGP/192.168.0.3

在InetAddress类中覆盖了Object类的toString方法,分析源码我们可以知道InetAddress中的toString方法返回了用“/“隔开的主机名和IP地址。因此,在上面的代码中直接通过localAddress对象来输出本机计算机名和IP地址(将对象参数传入println方法后,println方法会调用对象参数的toString方法来输出结果)。

当本机绑定了多个IP时,getLocalHost只返回第一个IP。如果想返回本机全部的IP,可以使用getAllByName方法。

getByName()方法

这个方法是InetAddress类最常用的方法。它可以通过指定域名从DNS中得到相应的IP地址。getByName一个String类型参数,可以通过这个参数指定远程主机的域名,它的定义如下:

public static InetAddress getByName(String host) throws UnknownHostException

如果host所指的域名对应多个IP,getByName返回第一个IP。如果本机名已知,可以使用getByName方法来代替getLocalHost。当host的值是localhost时,返回的IP一般是127.0.0.1。如果host是不存在的域名,getByName将抛出UnknownHostException异常,如果host是IP地址,无论这个IP地址是否存在,getByName方法都会返回这个IP地址(因此getByName并不验证IP地址的正确性)。下面代码演示了如何使用getByName方法:

import java.net.InetAddress;

/**
 * InetAddres测试类
 * 
 * @author 小明
 *
 */
public class MyInetAddress {
    public static void main(String[] args) throws Exception {
        String host = "www.baidu.com";
        InetAddress address = InetAddress.getByName(host);
        System.out.println(address);
    }
}

运行结果:

www.baidu.com/180.97.33.107

getByName方法除了可以使用域名作为参数外,也可以直接使用IP地址作为参数。如果使用IP地址作为参数,输出InetAddress对象时域名为空(除非调用getHostName方法后,再输出InetAddress对象。getHostName方法将在下面的内容介绍)。

getAllByName()方法

使用getAllByName方法可以从DNS上得到域名对应的所有的IP。这个方法返回一个InetAddress类型的数组。这个方法的定义如下:

  public static InetAddress[] getAllByName(String host) throws UnknownHostException

与getByName方法一样,当host不存在时,getAllByName也会抛出UnknowHostException异常,getAllByName也不会验证IP地址是否存在。下面的代码演示了getAllByName的用法:

import java.net.InetAddress;

/**
 * InetAddres测试类
 * 
 * @author 小明
 *
 */
public class MyInetAddress {
    public static void main(String[] args) throws Exception {
        String host = "www.baidu.com";
        InetAddress addresses[] = InetAddress.getAllByName(host);
        for (InetAddress address : addresses)
            System.out.println(address);
    }
}

运行结果:

www.baidu.com/180.97.33.107
www.baidu.com/180.97.33.108

其实getByName方法返回的IP地址就是getAllByName方法返回的第一个IP地址。

getByAddress()方法

这个方法必须通过IP地址来创建InetAddress对象,而且IP地址必须是byte数组形式。getByAddress方法有两个重载形式,定义如下:

public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException

第一个重载形式只需要传递byte数组形式的IP地址,getByAddress方法并不验证这个IP地址是否存在,只是简单地创建一个InetAddress对象。addr数组的长度必须是4(IPv4)或16(IPv6),如果是其他长度的byte数组,getByAddress将抛出一个UnknownHostException异常。第二个重载形式多了一个host,这个host和getByName、getAllByName方法中的host的意义不同,getByAddress方法并不使用host在DNS上查找IP地址,这个host只是一个用于表示addr的别名。下面的代码演示了getByAddress的两个重载形式的用。

import java.net.InetAddress;

/**
 * InetAddres测试类
 * 
 * @author 小明
 *
 */
public class MyInetAddress {
    public static void main(String[] args) throws Exception {
        byte ip[] = new byte[] { (byte) 180, (byte) 97, 33, 108 };
        InetAddress address1 = InetAddress.getByAddress(ip);
        InetAddress address2 = InetAddress.getByAddress("百度官方网站", ip);
        System.out.println(address1);
        System.out.println(address2);
    }
}

运行结果:

/180.97.33.102
百度官方网站/180.97.33.102

TCP编程

在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难,具体点说,Java语言无法实现底层的网络嗅探以及获得IP包结构等信息。

下面分别介绍一下在Java语言中客户端和服务器端的实现步骤。

客户端编程

连接服务器

在客户端可以通过两种方式来连接服务器,一种是通过IP的方式来连接服务器,而另外一种是通过域名方式来连接服务器。

其实这两种方式从本质上来看是一种方式。在底层客户端都是通过IP来连接服务器的,但这两种方式有一定的差异,如果通过IP方式来连接服务端程序,客户端只简单地根据IP进行连接,如果通过域名来连接服务器,客户端必须通过DNS将域名解析成IP,然后再根据这个IP来进行连接。

在Java中已经将域名解析功能包含在了Socket类中,因此,我们只需象使用IP一样使用域名即可。
通过Socket类连接服务器程序最常用的方法就是通过Socket类的构造函数将IP或域名以及端口号作为参数传入Socket类中。Socket类的构造函数有很多重载形式,最常用的一种形式:public Socket(String host, int port)。从这个构造函数的定义来看,只需要将IP或域名以及端口号直接传入构造函数即可。下面的代码是一个连接服务端程序的例子程序:

import java.net.Socket;

/**
 * 测试Socket类
 * 
 * @author 小明
 *
 */
public class MyConnection {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("www.baidu.com", 80);
        System.out.println(socket.getInetAddress().getHostName() + "已连接成功!");
    }
}

运行结果:

www.baidu.com已连接成功!

发送和接收数据

在Socket类中最重要的两个方法就是getInputStream和getOutputStream。这两个方法分别用来得到用于读取和写入数据的InputStream和OutputStream对象。在这里的InputStream读取的是服务器程序向客户端发送过来的数据,而OutputStream是客户端要向服务端程序发送的数据。

在编写实际的网络客户端程序时,是使用getInputStream(),还是使用getOutputStream(),以及先使用谁后使用谁由具体的应用决定。如通过连接网易网站(www.163.com)的80端口(一般为HTTP协议所使用的默认端口),并且发送一个字符串,最后再读取从www.163.com返回的信息:

import java.io.*;
import java.net.Socket;

/**
 * 测试Socket类
 * 
 * @author 小明
 *
 */
public class MyConnection {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("www.126.com", 80);
        // 向服务端程序发送数据
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                socket.getOutputStream()));
        writer.write("test\r\n\r\n");
        writer.flush();

        // 从服务端程序接收数据
        BufferedReader reader = new BufferedReader(new InputStreamReader(
                socket.getInputStream()));
        String str = "";
        while ((str = reader.readLine()) != null)
            System.out.println(str);
        socket.close();
    }
}

运行结果:

HTTP/1.0 500 Server Error
Cache-Control: no-cache
Connection: close
Content-Type: text/html

<html><body><h1>500 Server Error</h1>
An internal server error occured.
</body></html>

说明:

  1. 为了提高数据传输的效率,Socket类并没有在每次调用write方法后都进行数据传输,而是将这些要传输的数据写到一个缓冲区里(默认是8192个字节),然后通过flush方法将这个缓冲区里的数据一起发送出去,因此,writer.flush();是必须的。

  2. 在发送字符串时之所以在Hello World后加上 “\r\n\r\n”,这是因为HTTP协议头是以“\r\n\r\n”作为结束标志(HTTP协议的详细内容请参考此链接文章),因此,通过在发送字符串后加入“\r\n\r\n”,可以使服务端程序认为HTTP头已经结束,可以处理了。如果不加“\r\n\r\n”,那么服务端程序将一直等待HTTP头的结束,也就是“\r\n\r\n”。如果是这样,服务端程序就不会向客户端发送响应信息,而reader.readLine()将因无法读以响应信息面被阻塞,直到连接超时。

关闭网络连接

可以引起网络连接关闭的情况有以下4种:

  1. 直接调用Socket类的close方法。
  2. 只要Socket类的InputStream和OutputStream有一个关闭,网络连接自动关闭(必须通过调用InputStream和OutputStream的close方法关闭流,才能使网络连接自动关闭)。
  3. 在程序退出时网络连接自动关闭。
  4. 将Socket对象设为null或未关闭最使用new Socket(…)建立新对象后,由JVM的垃圾回收器回收为Socket对象分配的内存空间后自动关闭网络连接。

虽然这4种方法都可以达到同样的目的,但一个健壮的网络程序最好使用第1种或第2种方法关闭网络连接。这是因为第3种和第4种方法一般并不会马上关闭网络连接

服务器编程

创建ServerSocket对象

ServerSocket类的构造方法有四种重载形式,它们的定义如下:

public ServerSocket() throws IOException
public ServerSocket(int port) throws IOException
public ServerSocket(int port, int backlog) throws IOException
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

各参数:port是ServerSocket对象要绑定的端口,backlog是请求队列的长度,bindAddr是ServerSocket对象要绑定的IP地址。

通过构造方法绑定端口是创建ServerSocket对象最常用的方式:

public ServerSocket(int port) throws IOException

如果port参数所指定的端口已经被绑定,构造方法就会抛出IOException异常。但实际上抛出的异常是BindException。

如果port的值为0,系统就会随机选取一个端口号。但随机选取的端口意义不大,因为客户端在连接服务器时需要明确知道服务端程序的端口号。可以通过ServerSocket的toString方法输出和ServerSocket对象相关的信息。下面的代码输入了和ServerSocket对象相关的信息:

import java.io.IOException;
import java.net.ServerSocket;

/**
 * ServerSocket测试
 * 
 * @author 小明
 *
 */
public class ServerSocketTest {

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(30001);
        System.out.println(serverSocket);
    }
}

运行结果:

ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=30001]

addr是服务端绑定的IP地址,如果未绑定IP地址,这个值是0.0.0.0,在这种情况下,ServerSocket对象将监听服务端所有网络接口的所有IP地址,port永远是0。localport是ServerSocket绑定的端口,如果port值为0,localport是一个随机选取的端口号。

在操作系统中规定1 ~ 1023为系统使用的端口号。端口号的最小值是1,最大值是65535。在Windows中用户编写的程序可以绑定端口号小于1024的端口,但在Linux/Unix下必须使用root登录才可以绑定小于1024的端口。

获得连接

在使用ServerSocket对象绑定一个端口后,操作系统就会为这个端口分配一个先进先出的队列(这个队列长度的默认值一般是50),这个队列用于保存未处理的客户端请求,因此叫请求队列。而ServerSocket类的accept方法负责从这个队列中读取未处理的客户端请求。如果请求队列为空,accept则处于阻塞状态。每当客户端向服务端发来一个请求,服务端会首先将这个客户端请求保存在请求队列中,然后accept再从请求队列中读取。如果请求队列中的客户端请求数达到请求队列的最大容量时,服务端将无法再接收客户端请求。如果这时客户端再向服务端发请求,客户端将会抛出一个SocketException异常。

在服务端接收和发送数据

在建立完ServerSocket对象后,通过accept方法返回的Socket对象,服务端就可以和客户端进行数据交互。

Socket类有两个得到输入输出流的方法:getInputStream和getOutputStream。对于位于客户端的Socket引用而言,使用getInputStream方法得到的InputStream是从服务端获取数据,而getOutputStream方法得到的OutputStream是向服务端发送数据。而服务器端的Socket引用也是类似的,InputStream从客户端读取数据,OutputStream向客户端发送数据。

import java.net.*;
import java.io.*;

/**
 * ServerSocket测试
 * 
 * @author 小明
 *
 */
public class Server extends Thread {
    private Socket socket;

    public void run() {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
                    socket.getOutputStream()));
            writer.write("HTTP/1.1 200 OK\r\n\r\n");
            String str = "";
            while (!"".equals((str = reader.readLine()))){
                writer.write("" + str + "
"); } writer.flush(); socket.close(); } catch (Exception e) { throw new RuntimeException(e); } } public Server(Socket socket) { this.socket = socket; } public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8080); System.out.println("服务器已经启动,端口:8080"); while (true) { Socket socket = serverSocket.accept(); // 每接收到一个客户端的Socket引用,放入一个线程中处理 new Server(socket).start(); } } }

打开浏览器,在地址栏中输入:http://localhost:8080,浏览器中我们可以看到如下结果:

GET / HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8,en;q=0.6

在接收客户端请求后,只向客户端输出了一行HTTP响应头信息(包括响应码和HTTP版本号),对于HTTP响应头来说,这一行是必须有的,其他的头字段都是可选的。上面的代码每读一行请求头信息,就向客户端写一行响应信息。最后使用了flush方法将输出缓冲区中的内容发送的客户端,这是必须的。

关闭服务器端连接

在客户端和服务端的数据交互完成后,一般需要关闭网络连接。对于服务端来说,需要关闭Socket和ServerSocket。

在关闭Socket后,客户端并不会马上感知自已的Socket已经关闭,也就是说,在服务端的Socket关闭后,客户端的Socket的isClosed和isConnected方法仍然会分别得到false和true。但对已关闭的Socket的输入输出流进行操作会抛出一个SocketException异常。

在关闭服务端的ServerSocket后,ServerSocket对象所绑定的端口被释放。这时客户端将无法连接服务端程序。

TCP编程示例

使用多线程网络编程实现一个客户端与服务器端的消息发送与接收

服务器端

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * 服务器端窗体类
 * 
 * @author 小明
 *
 */
public class Server extends JFrame {

    private static final long serialVersionUID = 6206072788856936392L;

    private JTextArea info; // 显示消息的文本区
    private Socket socket; // 客户端Socket对象

    public Server(String title) {
        super(title); // 标题

        setSize(200, 300); // 窗体大小
        setLocation(200, 300); // 窗体坐标
        setDefaultCloseOperation(EXIT_ON_CLOSE); // 默认关闭操作

        info = new JTextArea(); // 创建文本区对象
        info.setEditable(false); // 文本区不可编辑
        info.setFocusable(false); // 文本区不可获得焦点
        JScrollPane jsp = new JScrollPane(info); // 创建滚动面板
        this.add(jsp, BorderLayout.CENTER); // 将滚动面板添加到窗体中

        JPanel pnl = new JPanel(new BorderLayout()); // 创建面板
        final JTextField msg = new JTextField(); // 创建输入文本框
        JButton send = new JButton("发送"); // 创建发送按钮
        pnl.add(msg, BorderLayout.CENTER); // 将输入文本框添加到面板
        pnl.add(send, BorderLayout.EAST); // 将发送按钮添加到面板
        this.add(pnl, BorderLayout.SOUTH); // 将面板添加到窗体中

        setVisible(true); // 设置窗体可见

        startServer(); // 启动服务器

        // 注册“发送”按钮的事件监听器
        send.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    // 创建缓冲区字节输出流对象
                    BufferedOutputStream out = new BufferedOutputStream(socket
                            .getOutputStream());
                    // 写入输出流
                    out.write((msg.getText() + "\n").getBytes());
                    // 刷出到客户端
                    out.flush();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
    }

    /**
     * 启动服务器
     */
    private void startServer() {
        try {
            // 创建ServerSocket对象,指定监听的端口
            ServerSocket server = new ServerSocket(30000);
            // 等待客户端连接
            socket = server.accept();
            // 读客户端发送消息
            read();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 读客户端消息
     */
    private void read() {
        try {
            // 创建缓冲区字符输入流
            final BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            // 开启新线程用于专门读数据
            new Thread(new Runnable() {
                @Override
                public void run() {
                    char[] ch = new char[1024];
                    // 读输入流中的数据
                    while (true) {
                        int len;
                        try {
                            len = reader.read(ch);
                            info.append(new String(ch, 0, len)); // 追加到文本区显示
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }).start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        new Server("服务器");
    }
}

客户端

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.Socket;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;

/**
 * 客户端窗体类
 * 
 * @author 小明
 *
 */
public class Client extends JFrame {

    private static final long serialVersionUID = 1173239888462689029L;

    private JTextArea info; // 显示消息的文本区
    private Socket socket; // 客户端Socket对象

    public Client(String title) {
        super(title); // 标题

        setSize(200, 300); // 窗体大小
        setLocation(700, 300); // 窗体坐标
        setDefaultCloseOperation(EXIT_ON_CLOSE); // 默认关闭操作

        info = new JTextArea(); // 创建文本区对象
        info.setEditable(false); // 文本区不可编辑
        info.setFocusable(false); // 文本区不可获得焦点
        JScrollPane jsp = new JScrollPane(info); // 创建滚动面板
        this.add(jsp, BorderLayout.CENTER); // 将滚动面板添加到窗体中

        JPanel pnl = new JPanel(new BorderLayout()); // 创建面板
        final JTextField msg = new JTextField(); // 创建输入文本框
        JButton send = new JButton("发送"); // 创建发送按钮
        pnl.add(msg, BorderLayout.CENTER); // 将输入文本框添加到面板
        pnl.add(send, BorderLayout.EAST); // 将发送按钮添加到面板
        this.add(pnl, BorderLayout.SOUTH); // 将面板添加到窗体中

        setVisible(true); // 设置窗体可见

        startClient(); // 启动客户端

        // 注册“发送”按钮的事件监听器
        send.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    // 创建缓冲区字节输出流对象
                    BufferedOutputStream out = new BufferedOutputStream(socket
                            .getOutputStream());
                    // 写入输出流
                    out.write((msg.getText() + "\n").getBytes());
                    // 刷出到服务器端
                    out.flush();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }
        });
    }

    /**
     * 启动客户端
     */
    private void startClient() {
        try {
            // 创建Socket对象,指定连接的服务器与连接的端口
            socket = new Socket("127.0.0.1", 30000);
            // 读服务端发送消息
            read();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 读服务器端消息
     */
    private void read() {
        try {
            // 创建缓冲区字符输入流
            final BufferedReader reader = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            // 开启新线程用于专门读数据
            new Thread(new Runnable() {
                @Override
                public void run() {
                    char[] ch = new char[1024];
                    // 读输入流中的数据
                    while (true) {
                        int len;
                        try {
                            len = reader.read(ch);
                            info.append(new String(ch, 0, len)); // 追加到文本区显示
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            }).start();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        new Client("客户端");
    }
}

运行效果图:

客户端效果图

服务器效果图

UDP编程

UDP(用户数据报协议)是用于将二进制数据从一台计算机发送到另一台计算机的非连接协议。在Java中数据报包的发送者与接收者都使用java.net.DatagramSocket类发送和接收包。

DatagramSocket的构造方法如下:

  • public DatagramSocket() throws SocketException
  • public DatagramSocket(SocketAddress bindaddr) throws SocketException
  • public DatagramSocket(int port) throws SocketException
  • public DatagramSocket(int port, InetAddress laddr) throws SocketException

构造方法中各参数的意思:port是绑定到本地主机指定的端口,bindaddr是封装了服务器和端口号的本地套接字地址,laddr是要绑定的本地地址。

常用方法:

  • public void send(DatagramPacket p) throws IOException
  • public void receive(DatagramPacket p) throws IOException

这两个方法都带有一个DatagramPacket参数。DatagramPacket代表一个数据报包,与DatagramSocket类似,包的发送者与接收者都要使用到它。DatagramPacket有六个构造方法,其中两个由接收者使用,四个由发送者使用。

接收数据报包:

  • public DatagramPacket(byte[] buf, int length):用来接收长度为 length 的数据包,buf为保存传入数据报的缓冲区。
  • public DatagramPacket(byte[] buf, int offset, int length):用来接收长度为 length 的包,在缓冲区buf中指定了偏移量offset。

发送数据报包:

  • public DatagramPacket(byte[] buf, int length, InetAddress address, int port)
  • public DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port)
  • public DatagramPacket(byte[] buf, int length, SocketAddress address) throws SocketException
  • public DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) throws SocketException

这几个方法都是将长度为 length 的包发送到指定主机上的指定端口号。

接收数据报包

接收数据报包的执行步骤如下:

  1. 创建一个足够大的字节数组,用于存储要接收的数据报包的数据。
  2. 使用该字节数组实例化一个DatagramPacket对象。
  3. DatagramSocket被实例化,它被指定到套接字要绑定的本地主机上的一个端口。
  4. 调用DatagramSocket类的receive()方法,将DatagramPacket对象传递到方法中,这将导致执行线程阻塞,直到接收一个数据报包或超时。

在reveive()方法返回后,新包刚传送成功,如果发生了超时,receive()方法不会返回,而是抛出异常。DatagramPacket类的getData()方法可以用于接收包含该数据报包数据的字节数组。

发送数据报包

发送数据报包的执行步骤如下:

  1. 创建一个足够大的字节数组,用于存储要发送的数据报包的数据。
  2. 创建DatagramPacket对象,用于存储该字节数组,以及服务器的名称和接收者的端口号。
  3. DatagramSocket被实例化,它被指定到套接字要绑定的本地主机上的一个端口。
  4. 调用DatagramSocket类的send()方法,传递DatagramPacket对象参数,发送数据报包。

UDP编程示例

发送者:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * 发送者
 * 
 * @author 小明
 *
 */
public class Sender {

    public static void main(String[] args) throws IOException {
        // 创建字节数组
        byte[] buf = "我是发送者发送的数据".getBytes();
        // 创建DatagramPacket对象
        DatagramPacket packet = new DatagramPacket(buf, buf.length,
                InetAddress.getLocalHost(), 30000);
        // 创建DatagramSocket对象
        DatagramSocket socket = new DatagramSocket(30001);
        System.out.println("发送一个数据报包......");
        // 发送数据报包
        socket.send(packet);
        System.out.println("已向" + packet.getSocketAddress() + "发送数据报包");
        // 获取数据报包数据
        buf = packet.getData();
        System.out.println("发送的数据:" + new String(buf));
    }
}

接收者:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

/**
 * 接收者
 * 
 * @author 小明
 *
 */
public class Receiver {
    public static void main(String[] args) throws IOException {
        // 创建字节数组
        byte[] buf = new byte[1024];
        // 创建DatagramPacket对象
        DatagramPacket packet = new DatagramPacket(buf, buf.length);
        // 创建DatagramSocket对象
        DatagramSocket socket = new DatagramSocket(30000);
        System.out.println("等待一个数据报包......");
        // 接收数据报包
        socket.receive(packet);
        System.out.println("从" + packet.getSocketAddress() + "接收到包");
        // 获取数据报包数据
        buf = packet.getData();
        System.out.println("接收到数据:" + new String(buf, 0, packet.getLength()));
    }
}

运行结果:

发送者:

发送一个数据报包......
已向NNOYHM42DZMMTGP/192.168.0.3:30000发送数据报包
发送的数据:我是发送者发送的数据

接收者:

等待一个数据报包......
从/192.168.0.3:30001接收到包
接收到数据:我是发送者发送的数据
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值