@lemonguge
2015-07-02T04:25:00.000000Z
字数 8677
阅读 505
Socket
我写这篇文章是为了写nio做准备的,所以不会写的很深入,但是看完之后相信大家会对Java网络编程有一定的了解,网络编程所用到的类都在java.net包中。
首先我们有必要了解两种网络模型,因为接下来所学的都将是传输层的知识。OSI(开放系统互连)七层参考模型和TCP/IP四层参考模型,其中TCP/IP模型是从OSI模型上发展而来,因此这两种协议有很大的相似度。OSI模型是在协议开发前设计的,具有通用性;TCP/IP是先有协议集然后建立模型,不适用于非TCP/IP网络。

接下来,来看看OSI参考模型每一层的功能:
对上面这七层的应用简单来说,你的朋友给你的QQ发了一张图片,首先通过各种通讯媒介(物理层),到你这边通过交换机(数据链路层)后,接着是路由器(网络层),使用UDP(传输层)的方式,对你的QQ进行会话请求,你的QQ一旦接受的会话请求(会话层,本机其他人的QQ就看不到),接着传来了一张图片(表现层),通过我们自己的QQ(应用层)将看到这张图片。
OSI参考模型的每一层使用下层提供的服务,并向其上层提供服务。
OSI参考模型与TCP/IP参考模型的传输层功能基本相似,都是负责为用户提供真正的端对端的通信服务,也对高层屏蔽了底层网络的实现细节。所不同的是TCP/IP参考模型的传输层是建立在网络互联层基础之上的,而网络互联层只提供无连接的网络服务,所以面向连接的功能完全在TCP协议中实现,当然TCP/IP的传输层还提供无连接的服务,如UDP;相反OSI参考模型的传输层是建立在网络层基础之上的,网络层既提供面向连接的服务,又提供无连接的服务,但传输层只提供面向连接的服务。
ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。相反,TCP/IP参考模型虽然有许多不尽人意的地方,但还是比较成功的。
我们的设备有一个默认的IP地址:127.0.0.1(本机回环地址)和主机名:localhost。可以通过CMD的命令ping 127.0.0.1查看网卡是否有效,由于IP地址不方便记忆,所以可以使用主机名来代替,就是说可以通过ping localhost来代替IP地址。这些所说的IP地址对是对内的,对外而言,会还有一个唯一的IP地址。
java.net.InetAddress表示互联网协议 (IP) 地址,处于TPC/IP模型中的网络互连层(网际层)。由于IPv4(互联网协议第4版)的资源不够用,所以后来发展了IPv6,这两种IP都有相应的对应类。通过查看API可以发现InetAddress类并没有默认的构造函数,也就是说我们不能通过new的方式创建对象,以下是一个简单的IP展示代码:
import java.net.InetAddress;import java.net.UnknownHostException;public class IPDemo {public static void main(String[] args) throws UnknownHostException {easyIPShow();}public static void easyIPShow() throws UnknownHostException {//获取本机回环地址,Java SE7出现的方法InetAddress ip = InetAddress.getLoopbackAddress();// IPv4返回127.0.0.1,IPv6返回::1System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());//获取本地主机IP地址对象ip = InetAddress.getLocalHost();System.out.println(ip.getHostAddress()); // 返回IP地址字符串System.out.println(ip.getHostName()); // 返回IP地址的主机名}} /* Output:127.0.0.1localhost10.10.7.53JieHong-PC*///:~
以上演示的是获取本机的IP地址,当然我们也可以获取其他主机的IP地址,通过主机名或者IP地址字符串。
import java.net.InetAddress;import java.net.UnknownHostException;public class IPDemo {public static void main(String[] args) throws UnknownHostException {// 在给定主机名(或IP地址字符串)的情况下确定主机的IP地址。getIPByName();}public static void getIPByName() throws UnknownHostException {InetAddress ip = InetAddress.getByName("JieHong-PC");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());ip = InetAddress.getByName("10.10.7.53");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());}} /* Output:10.10.7.53JieHong-PC10.10.7.53JieHong-PC*///:~
甚至我们可以通过网站名获取IP地址字符串,以下是一个小展示:
import java.net.InetAddress;import java.net.UnknownHostException;public class IPDemo {public static void main(String[] args) throws UnknownHostException {hostNameShow();}public static void hostNameShow() throws UnknownHostException {InetAddress ip = InetAddress.getByName("www.sina.com.cn");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());ip = InetAddress.getByName("www.baidu.com");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());}} /* Output:202.108.33.98www.sina.com.cn61.135.169.121www.baidu.com*///:~
在这里有必要多说一下上面方式的工作原来,"www.sina.com.cn"的构成是这样的:“主机名.域名.组织.国家”,".com"代表一个商业组织,".org"非盈利组织。除了"www"这种主机名,我们还能看到"tieba.baidu.com"百度贴吧,"tech.qq.com"腾讯科技等很多不同的主机名。我们首先通过DNS域名解析服务器,将"baidu"解析成"61.135.169.121",之后通过该IP找到百度,根据主机名访问不同的资源。为什么无需指定端口号,因为默认为80端口。因此我们可以通过百度的IP直接进行访问,即在浏览器中输入"61.135.169.121"便可以跳转到百度。注意,只有百度可以直接输入IP进行访问,类似新浪、腾讯等其他网站直接输入IP会被禁止"Access Denied"。
所谓的域名解析,其实很简单,只是存储了大量的IP和网站名的对应列表,在我们进行域名解析的时候,首先会走本地域名解析列表,之后才进行DNS。正是由于会先走本地的域名解析列表,所以可以通过添加自定义域名解析列表来屏蔽一些垃圾网站(使用本机回环地址127.0.0.1与网站名对应),本地域名解析列表在"C:\Windows\System32\drivers\etc"的hosts文件中。
接下来可以在该hosts文件中依次添加两行:"127.0.0.1 www.jiehong.com"和"192.168.1.100 www.game18.com"。
import java.net.InetAddress;import java.net.UnknownHostException;public class IPDemo {public static void main(String[] args) throws UnknownHostException {hideWeb();}public static void hideWeb() throws UnknownHostException {InetAddress ip = InetAddress.getByName("www.jiehong.com");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());ip = InetAddress.getByName("www.game18.com");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());ip = InetAddress.getByName("192.168.1.100");System.out.println(ip.getHostAddress());System.out.println(ip.getHostName());}} /* Output:127.0.0.1www.jiehong.com192.168.1.100www.game18.com192.168.1.100www.game18.com*///:~
如果在这两行中间再添加一行"192.168.1.100 www.game19.com",那么最后一行是输出结果将为"www.game19.com",只要一查到符合的信息便不会再进行查找。
当你挂着QQ聊天的时候,数据发到你这个IP,需要指明被哪个进程接收,是QQ还是浏览器。端口号用于标识进程的逻辑地址,不同进程的标识不同。有效的端口从0到65535,其中0~1024系统使用或保留端口。
通讯的规则,常见的传输协议有两种:TCP和UDP。
以上这两种通讯协议都在Java中有对应的类,在讲解这些类之前应该先了解一下Socekt。
Socket的英文翻译为插座,我是认为这个翻译是比较恶心的,专业术语是套接字。网络都必须有接收端和发送端,只有一端那只能叫做单机了。每个端点如果想进行通讯都必须有Socket,套接字是一种通信机制,不同的通信方式有不同的Socket。举个例子:你从厦门到鼓浪屿的话,会从你这边海的码头坐船到另一边的码头,这两个码头就相当与Socket。
在Java中将UDP这种协议进行了封装,使得方便我们操作。UDP传输协议对应的端点服务Socket为DatagramSocket,此类表示用来发送和接收数据报包的套接字。该类的send和receive方法将从此套接字发送和接收数据报包,DatagramPacket表示数据报包,实现无连接包投递服务,不对包投递做出保证。由于所包含的信息太多,例如目的地址和源地址、校验码、包中的数据等,所以将数据封装成对象来方便我们操作。通过查看API可以发现DatagramPacket的构造函数分出两类,一类用来发送数据,一类用来接收数据。注意,发送数据的数据报包都应该明确目的地址,即构造函数中会具有含IP对象的参数。
清楚了上面所说的内容,首先我们来明确创建UDP传输协议的发送端的思路:
import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;public class UPDSend {public static void main(String[] args) throws IOException {// 构造发送端数据报套接字,并将其绑定到本地主机上任何可用的端口DatagramSocket ds = new DatagramSocket();// 将要发送的数据封装到数据报包中String data = "UPD传输协议:发送端启动";byte[] buf = data.getBytes();// 数据报包将发送到指定主机上的指定端口号DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("JieHong-PC"), 8899);// 发送数据报包ds.send(dp);// 关闭此数据报套接字以释放资源ds.close();}} ///:OK~
创建UDP传输协议的接收端的思路:
import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;public class UDPReceive {public static void main(String[] args) throws IOException {// 监听绑定到本地主机上的指定端口,此时与发送的数据报包中的端口一致DatagramSocket ds = new DatagramSocket(8899);// 创建数据报包,用于存储接收到的数据byte[] buf = new byte[1024];DatagramPacket dp = new DatagramPacket(buf, buf.length);// 将接收的数据存储到数据包中ds.receive(dp); // 阻塞式方法// 解析数据报包的数据String ip = dp.getAddress().getHostAddress();int port = dp.getPort(); // 返回发送端绑定的端口号String data = new String(dp.getData(), 0, dp.getLength()); // 获取接收到的数据的长度System.out.println(ip + ":" + port + ":" + data);// 关闭此数据报套接字以释放资源ds.close();}} /* Output: // 只有先启动接收端,后启动发送端才能看到以下输出结果10.10.7.53:52684:UPD传输协议:发送端启动*///:~
通过发送端随机绑定了52684端口,当然我们也可以在发送端的Socket构造函数中指定端口,或者使用bind方法绑定端口。
笔者将演示使用多线程来实现一个聊天室的功能,在主控台输入后显示,当输入886之后退出聊天。
import java.io.BufferedReader;import java.io.IOException;import java.io.InputStreamReader;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;// 当发送端发送了886之后便退出聊天class Send implements Runnable{private DatagramSocket ds;public Send(DatagramSocket ds) { this.ds = ds; }@Overridepublic void run() {BufferedReader br = new BufferedReader(new InputStreamReader(System.in));String line = null;try {while ((line = br.readLine()) != null) {byte[] buf = line.getBytes();DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(), 8010);ds.send(dp);if ("886".equals(line))break;}} catch (IOException e) { }}}// 接收端class Receive implements Runnable{private DatagramSocket ds;public Receive(DatagramSocket ds) { this.ds = ds; }@Overridepublic void run() {while(true){byte[] buf = new byte[1024];DatagramPacket dp = new DatagramPacket(buf, buf.length);try {ds.receive(dp); // 阻塞式方法String ip = dp.getAddress().getHostAddress();int port = dp.getPort();String data = new String(dp.getData(), 0, dp.getLength());System.out.println(ip + ":" + port + ":" + data);if ("886".equals(data)) {System.out.println(ip + ":退出聊天室");}} catch (IOException e) { }}}}public class Chat {public static void main(String[] args) throws IOException {// 绑定到本地主机上的指定端口DatagramSocket send = new DatagramSocket(9001);// 接收端需与发送的数据报包中的端口一致DatagramSocket receive = new DatagramSocket(8010);new Thread(new Send(send)).start();new Thread(new Receive(receive)).start();}} ///:0K~