Java中网络编程基础知识(转载/整理)(一)
IP地址和域名很好的解决了在网络中找到一个计算机的问题,但是为了让一个计算机可以同时运行多个网络程序,就引入了另外一个概念——端口(port)。
在介绍端口的概念以前,首先来看一个例子,一般一个公司前台会有一个电话,每个员工会有一个分机,这样如果需要找到这个员工的话,需要首先拨打前台总机,然后转该分机号即可。这样减少了公司的开销,也方便了每个员工。在该示例中前台总机的电话号码就相当于IP地址,而每个员工的分机号就相当于端口。
有了端口的概念以后,在同一个计算机中每个程序对应唯一的端口,这样一个计算机上就可以通过端口区分发送给每个端口的数据了,换句话说,也就是一个计算机上可以并发运行多个网络程序,而不会在互相之间产生干扰。
在硬件上规定,端口的号码必须位于0-65535之间,每个端口唯一的对应一个网络程序,一个网络程序可以使用多个端口。这样一个网络程序运行在一台计算上时,不管是客户端还是服务器,都是至少占用一个端口进行网络通讯。在接收数据时,首先发送给对应的计算机,然后计算机根据端口把数据转发给对应的程序。
有了IP地址和端口的概念以后,在进行网络通讯交换时,就可以通过IP地址查找到该台计算机,然后通过端口标识这台计算机上的一个唯一的程序。这样就可以进行网络数据的交换了。
在网络通讯中,第一次主动发起通讯的程序被称作客户端(Client)程序,简称客户端,而在第一次通讯中等待连接的程序被称作服务器端(Server)程序,简称服务器。一旦通讯建立,则客户端和服务器端完全一样,没有本质的区别。
由此,网络编程中的两种程序就分别是客户端和服务器端,例如QQ程序,每个QQ用户安装的都是QQ客户端程序,而QQ服务器端程序则运行在腾讯公司的机房中,为大量的QQ用户提供服务。这种网络编程的结构被称作客户端/服务器结构,也叫做Client/Server结构,简称C/S结构。
使用C/S结 构的程序,在开发时需要分别开发客户端和服务器端,这种结构的优势在于由于客户端是专门开发的,所以根据需要实现各种效果,专业点说就是表现力丰富,而服务器端也需要专门进行开发。但是这种结构也存在着很多不足,例如通用性差,几乎不能通用等,也就是说一种程序的客户端只能和对应的服务器端通讯,而不能和 其它服务器端通讯,在实际维护时,也需要维护专门的客户端和服务器端,维护的压力比较大。
其实在运行很多程序时,没有必要使用专用的客户端,而需要使用通用的客户端,例如浏览器,使用浏览器作为客户端的结构被称作浏览器/服务器结构,也叫做Browser/Server结构,简称为B/S结构。
使用B/S结构的程序,在开发时只需要开发服务器端即可,这种结构的优势在于开发的压力比较小,不需要维护客户端。但是这种结构也存在着很多不足,例如浏览器的限制比较大,表现力不强,无法进行系统级操作等。
总之C/S结构和B/S结构是现在网络编程中常见的两种结构,B/S结构其实也就是一种特殊的C/S结构。
另外简单的介绍一下P2P(Point to Point)程序,常见的如BT、电驴等。P2P程序是一种特殊的程序,应该一个P2P程序中既包含客户端程序,也包含服务器端程序,例如BT,使用客户端程序部分连接其它的种子(服务器端),而使用服务器端向其它的BT客户端传输数据。如果这个还不是很清楚,其实P2P程序和手机是一样的,当手机拨打电话时就是使用客户端的作用,而手机处于待机状态时,可以接收到其它用户拨打的电话则起的就是服务器端的功能,只是一般的手机不能同时使用拨打电话和接听电话的功能,而P2P程序实现了该功能。
最后再介绍一个网络编程中最重要,也是最复杂的概念——协议(Protocol)。按照前面的介绍,网络编程就是运行在不同计算机中两个程序之间的数据交换。在实际进行数据交换时,为了让接收端理解该数据,计算机比较笨,什么都不懂的,那么就需要规定该数据的格式,这个数据的格式就是协议。在实际的网络程序编程中,最麻烦的内容不是数据的发送和接收,因为这个功能在几乎所有的程序语言中都提供了封装好的API进行调用,最麻烦的内容就是协议的设计以及协议的生产和解析,这个才是网络编程中最核心的内容。
在现有的网络中,网络通讯的方式主要有两种:1 TCP(传输控制协议)方式;2 UDP(用户数据报协议)方式。
在网络通讯中,TCP方式就类似于拨打电话,使用该种方式进行网络通讯时,需要建立专门的虚拟连接,然后进行可靠的数据传输,如果数据发送失败,则客户端会自动重发该数据。而UDP方式就类似于发送短信,使用这种方式进行网络通讯时,不需要建立专门的虚拟连接,传输也不是很可靠,如果发送失败则客户端无法获得。
这两种传输方式都是实际的网络编程中进行使用,重要的数据一般使用TCP方式进行数据传输,而大量的非核心数据则都通过UDP方式进行传递,在一些程序中甚至结合使用这两种方式进行数据的传递。
由于TCP需要建立专用的虚拟连接以及确认传输是否正确,所以使用TCP方式的速度稍微慢一些,而且传输时产生的数据量要比UDP稍微大一些。
客户端网络编程步骤:
客户端(Client)是指网络编程中首先发起连接的程序,客户端一般实现程序界面和基本逻辑实现,在进行实际的客户端编程时,无论客户端复杂还是简单,以及客户端实现的方式,客户端的编程主要由三个步骤实现:
1、 建立网络连接
客户端网络编程的第一步都是建立网络连接。在建立网络连接时需要指定连接到的服务器的IP地址和端口号,建立完成以后,会形成一条虚拟的连接,后续的操作就可以通过该连接实现数据交换了。
2、 交换数据
连接建立以后,就可以通过这个连接交换数据了。交换数据严格按照请求响应模型进行,由客户端发送一个请求数据到服务器,服务器反馈一个响应数据给客户端,如果客户端不发送请求则服务器端就不响应。根据逻辑需要,可以多次交换数据,但是还是必须遵循请求响应模型。
3、 关闭网络连接
在数据交换完成以后,关闭网络连接,释放程序占用的端口、内存等系统资源,结束网络编程。
最基本的步骤一般都是这三个步骤,在实际实现时,步骤2会出现重复,在进行代码组织时,由于网络编程是比较耗时的操作,所以一般开启专门的现场进行网络通讯。
服务器端网络编程步骤
服务器端(Server)是指在网络编程中被动等待连接的程序,服务器端一般实现程序的核心逻辑以及数据存储等核心功能。服务器端的编程步骤和客户端不同,是由四个步骤实现,依次是:
1、 监听端口
服务器端属于被动等待连接,所以服务器端启动以后,不需要发起连接,而只需要监听本地计算机的某个固定端口即可。 这个端口就是服务器端开放给客户端的端口,服务器端程序运行的本地计算机的IP地址就是服务器端程序的IP地址。
2、 获得连接
当客户端连接到服务器端时,服务器端就可以获得一个连接,这个连接包含客户端的信息,例如客户端IP地址等等,服务器端和客户端也通过该连接进行数据交换。 一般在服务器端编程中,当获得连接时,需要开启专门的线程处理该连接,每个连接都由独立的线程实现。
3、 交换数据
服务器端通过获得的连接进行数据交换。服务器端的数据交换步骤是首先接收客户端发送过来的数据,然后进行逻辑处理,再把处理以后的结果数据发送给客户端。简单来说,就是先接收再发送,这个和客户端的数据交换数序不同。 其实,服务器端获得的连接和客户端连接是一样的,只是数据交换的步骤不同。 当然,服务器端的数据交换也是可以多次进行的。 在数据交换完成以后,关闭和客户端的连接。
4、 关闭连接
当服务器程序关闭时,需要关闭服务器端,通过关闭服务器端使得服务器监听的端口以及占用的内存可以释放出来,实现了连接的关闭。
其实服务器端编程的模型和呼叫中心的实现是类似的,例如移动的客服电话10086就是典型的呼叫中心,当一个用户拨打10086时,转接给一个专门的客服人员,由该客服实现和该用户的问题解决,当另外一个用户拨打10086时,则转接给另一个客服,实现问题解决,依次类推。 在服务器端编程时,10086这个电话号码就类似于服务器端的端口号码,每个用户就相当于一个客户端程序,每个客服人员就相当于服务器端启动的专门和客户端连接的线程,每个线程都是独立进行交互的。 这就是服务器端编程的模型,只是TCP方式是需要建立连接的,对于服务器端的压力比较大,而UDP是不需要建立连接的,对于服务器端的压力比较小罢了。
在Java语言中,对于TCP方式的网络编程提供了良好的支持,在实际实现时,以java.net.Socket类代表客户端连接,以java.net.ServerSocket类代表服务器端连接。在进行网络编程时,底层网络通讯的细节已经实现了比较高的封装,所以在程序员实际编程时,只需要指定IP地址和端口号码就可以建立连接了。正是由于这种高度的封装,一方面简化了Java语言网络编程的难度,另外也使得使用Java语言进行网络编程时无法深入到网络的底层,所以使用Java语言进行网络底层系统编程很困难,具体点说,Java语言无法实现底层的网络嗅探以及获得IP包结构等信息。但是由于Java语言的网络编程比较简单,所以还是获得了广泛的使用。PS. Stream Sockets利用数据流和文件IO技术,将计算机间的数据通讯视如对文件输入、输出流操作一样,进行用户和服务器之间的数据交流。
什么是套接字?此部分讨论详细出处参考http://blog.csdn.net/pengfeixiong/article/details/7466632
套接字,是支持TCP/IP的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。简单的说就是通信双方的一种约定,用套接字中的相关函数来完成通信过程。应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。多个TCP连接或多个应用程序进程可能需要通过同一个 TCP协议端口传输数据。为了区别不同的应用程序进程和连接,许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字(Socket)的接口。
区分不同应用程序进程间的网络通信和连接,主要有3个参数:通信的目的IP地址、使用的传输层协议(TCP或UDP)和使用的端口号。Socket原意是 “插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,应用层就可以和传输层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。
每一个基于TCP/IP网络通讯的程序(进程)都被赋予了唯一的端口和端口号,端口是一个信息缓冲区,用于保留Socket中的输入/输出信息,端口号是一个16位无符号整数,范围是0-65535,以区别主机上的每一个程序(端口号就像房屋中的房间号),低于256的端口号保留给标准应用程序,比如pop3的端口号就是110,每一个套接字都组合进了IP地址、端口、端口号,这样形成的整体就可以区别每一个套接字。
套接字的本质是通信过程中所要使用的一些缓冲区及一些相关的数据结构。
用InetAddress获得主机名和地址
import java.net.InetAddress; import java.net.UnknownHostException; public class Start { public static void main (String[] args) { InetAddress net1 = null; InetAddress net2 = null; try { net1 = InetAddress.getByName("www.baidu.com"); // net2 = InetAddress.getByName("127.0.0.1"); net2 = InetAddress.getLocalHost(); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(net1); System.out.println(net2); String hostname1 = net1.getHostName(); System.out.println("hostname: "+hostname1); String hostaddress1 = net1.getHostAddress(); System.out.println("hostaddress: "+hostaddress1); String hostname2 = net2.getHostName(); System.out.println("hostname: "+hostname2); String hostaddress2 = net2.getHostAddress(); System.out.println("hostaddress: "+hostaddress2); } }
运行结果:
www.baidu.com/119.75.218.70 xhj-PC/192.168.1.100 hostname: www.baidu.com hostaddress: 119.75.218.70 hostname: xhj-PC hostaddress: 192.168.1.100
一个最简单的Stream Socket(即基于TCP)通信的例子(客户端)
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; public class SimpleSocketClient { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Socket socket = null; InputStream inputStream = null; OutputStream outputStream = null; String serverIP = "127.0.0.1"; int serverPort = 10000; String out_message = "Hello"; try { // 建立连接到serverIP计算机的serverPort端口,如果建立连接时,服务程序未开启或本机网络不通时则抛出异常 socket = new Socket(serverIP, serverPort); // java中,数据传输功能由java IO实现,只需从连接中获得输入流或输出流 // socket.getInputStream/socket.getOutputStream outputStream = socket.getOutputStream(); // 发送数据 outputStream.write(out_message.getBytes()); inputStream = socket.getInputStream(); // 接收数据 byte[] in_message = new byte[1024]; int n = inputStream.read(in_message); // 打印接收到的数据 System.out.println(new String(in_message, 0, n)); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { // 关闭流和连接 inputStream.close(); outputStream.close(); socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
一个最简单的Stream Socket(即基于TCP)通信的例子(服务器端)
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class SimpleSocketServer { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub ServerSocket serverSocket = null; Socket socket = null; OutputStream outputStream = null; InputStream inputStream = null; int port = 10000; String out_message = "World"; try { // 由于服务器的实现是被动等待连接,所以服务器端的第一个步骤就是监听端口,也就是监听客户端是否有连接到达 serverSocket = new ServerSocket(port); // 第二步,获得连接。当有客户端连接到达时,建立一个与客户端连接对应的socket对象,从而释放客户端连接对于服务器端口的占用 // accept方法以及io流中的read方法是阻塞方法,当无连接时,该方法将阻塞程序的执行直到连接到达 socket = serverSocket.accept(); // java中,数据传输功能由java IO实现,只需从连接中获得输入流或输出流 // socket.getInputStream/socket.getOutputStream inputStream = socket.getInputStream(); //接收数据 byte[] bytes = new byte[1024]; int length = inputStream.read(bytes); //将接受的字节数组转化成字符串 String in_message = new String(bytes, 0, length); //打印接收数据 System.out.println(in_message); //发送数据 outputStream = socket.getOutputStream(); outputStream.write(out_message.getBytes()); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { outputStream.close(); inputStream.close(); socket.close(); serverSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
PS. 先运行服务器端程序,在运行客户端程序,运行效果是客户端发送Hello,服务器端响应World。
在服务器端程序编程中,由于服务器端实现的是被动等待连接,所以服务器端编程的第一个步骤是监听端口,也就是监听是否有客户端连接到达。实现服务器端监听的代码为:
ServerSocket ss = new ServerSocket(10000);
该代码实现的功能是监听当前计算机的10000号端口,如果在执行该代码时,10000号端口已经被别的程序占用,那么将抛出异常。否则将实现监听。
服务器端编程的第二个步骤是获得连接。该步骤的作用是当有客户端连接到达时,建立一个和客户端连接对应的Socket连 接对象,从而释放客户端连接对于服务器端端口的占用。实现功能就像公司的前台一样,当一个客户到达公司时,会告诉前台我找某某某,然后前台就通知某某某, 然后就可以继续接待其它客户了。通过获得连接,使得客户端的连接在服务器端获得了保持,另外使得服务器端的端口释放出来,可以继续等待其它的客户端连接。 实现获得连接的代码是:
Socket socket = ss.accept();
该代码实现的功能是获得当前连接到服务器端的客户端连接。需要说明的是accept和前面IO部分介绍的read方法一样,都是一个阻塞方法,也就是当无连接时,该方法将阻塞程序的执行,直到连接到达时才执行该行代码。另外获得的连接会在服务器端的该端口注册,这样以后就可以通过在服务器端的注册信息直接通信,而注册以后服务器端的端口就被释放出来,又可以继续接受其它的连接了。
基于Socket的C/S模式通信图
一个同样简单的Stream Socket(即基于TCP)通信的例子(客户端)
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.UnknownHostException; public class MulSocketClient { public static void main(String[] args) { Socket socket = null; InputStream inputStream = null; OutputStream outputStream = null; String serverIP = "127.0.0.1"; int port = 10000; String[] out_data = {"client_first", "client_second", "client_third"}; try { socket = new Socket(serverIP, port); outputStream = socket.getOutputStream(); inputStream = socket.getInputStream(); byte[] bytes = new byte[1024]; for (int i = 0; i < out_data.length; i++) { // 发送数据 outputStream.write(out_data[i].getBytes()); // 接收数据并显示 int n = inputStream.read(bytes); System.out.println(new String(bytes, 0, n)); } } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { outputStream.close(); inputStream.close(); socket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
一个同样简单的Stream Socket(即基于TCP)通信的例子(服务器端)
import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; public class MulSocketServer { /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub ServerSocket serverSocket = null; Socket socket = null; InputStream inputStream = null; OutputStream outputStream = null; String[] data_out = {"server_first", "server_second", "server_third"}; int port = 10000; try { serverSocket = new ServerSocket(port); socket = serverSocket.accept(); inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); byte[] bytes = new byte[1024]; for (int i = 0; i < data_out.length; i++) { int n = inputStream.read(bytes); System.out.println(new String(bytes, 0, n)); outputStream.write(data_out[i].getBytes()); } } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { outputStream.close(); inputStream.close(); socket.close(); serverSocket.close(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
PS. 只不过这个例子是多次发送和接收。
运行效果:客户端发送client_first client_second client_third到服务器端;服务器端发送server_first server_second server_third到客户端。