[关闭]
@lemonguge 2015-07-02T04:25:00.000000Z 字数 8677 阅读 505

Java网络编程(一)

Socket


我写这篇文章是为了写nio做准备的,所以不会写的很深入,但是看完之后相信大家会对Java网络编程有一定的了解,网络编程所用到的类都在java.net包中。


网络模型

首先我们有必要了解两种网络模型,因为接下来所学的都将是传输层的知识。OSI(开放系统互连)七层参考模型和TCP/IP四层参考模型,其中TCP/IP模型是从OSI模型上发展而来,因此这两种协议有很大的相似度。OSI模型是在协议开发前设计的,具有通用性;TCP/IP是先有协议集然后建立模型,不适用于非TCP/IP网络。

两种协议之间的对照

OSI参考模型

接下来,来看看OSI参考模型每一层的功能:

  1. 物理层:利用传输介质为数据链路层提供物理连接。主要关心的是通过物理链路从一个节点向另一个节点传送比特流(多少伏电压代表1?多少伏电压代表0?将1、0转化为转化为电流强弱来进行传输,到达目的地后再转化为1、0,也就是常说的数模转换与模数转化),物理层可能是铜线、卫星、微波或其他的通讯媒介
  2. 数据链路层:传送的协议数据单元称为数据帧,数据帧中包含物理地址(又称MAC地址)、控制码、数据及校验码等信息。该层的主要作用是通过校验、确认和反馈重发等手段,将不可靠的物理链路转换成对网络层来说无差错的数据链路。简单来说,就是主要进行MAC地址的封装与解封装,工作在这一层的设备是交换机
  3. 网络层:传送的协议数据单元称为数据包或分组。从下层接收到的数据进行IP地址的封装与解封装,工作在这一层的设备是路由器
  4. 传输层:传送的协议数据单元称为或报文。定义了一些传输数据的协议和端口号,如:TCP(传输控制协议,传输效率低,可靠性强,用于传输可靠性要求高,数据量大的数据),UDP(用户数据报协议,与TCP相反,用于传输可靠性要求不高,数量小的数据,如QQ聊天数据就是通过这种方式传输的)。
  5. 会话层:管理和协调不同主机上各种进程之间的通信(对话),即负责建立、管理和终止应用程序之间的会话,主要在用于发起会话或者接受会话请求。
  6. 表现层:主要是进行对接收的数据进行解释,将计算机能识别的东西转换成人能够识别的东西,如我们看到的图片、视频等。
  7. 应用层:主要是一些终端应用,如QQ、Chrome等应用。

对上面这七层的应用简单来说,你的朋友给你的QQ发了一张图片,首先通过各种通讯媒介(物理层),到你这边通过交换机(数据链路层)后,接着是路由器(网络层),使用UDP(传输层)的方式,对你的QQ进行会话请求,你的QQ一旦接受的会话请求(会话层,本机其他人的QQ就看不到),接着传来了一张图片(表现层),通过我们自己的QQ(应用层)将看到这张图片。

OSI参考模型的每一层使用下层提供的服务,并向其上层提供服务。

两种网络模型的比较

OSI参考模型与TCP/IP参考模型的传输层功能基本相似,都是负责为用户提供真正的端对端的通信服务,也对高层屏蔽了底层网络的实现细节。所不同的是TCP/IP参考模型的传输层是建立在网络互联层基础之上的,而网络互联层只提供无连接的网络服务,所以面向连接的功能完全在TCP协议中实现,当然TCP/IP的传输层还提供无连接的服务,如UDP;相反OSI参考模型的传输层是建立在网络层基础之上的,网络层既提供面向连接的服务,又提供无连接的服务,但传输层只提供面向连接的服务。

ISO制定的OSI参考模型的过于庞大、复杂招致了许多批评。相反,TCP/IP参考模型虽然有许多不尽人意的地方,但还是比较成功的。


网络通讯要素

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展示代码:

  1. import java.net.InetAddress;
  2. import java.net.UnknownHostException;
  3. public class IPDemo {
  4. public static void main(String[] args) throws UnknownHostException {
  5. easyIPShow();
  6. }
  7. public static void easyIPShow() throws UnknownHostException {
  8. //获取本机回环地址,Java SE7出现的方法
  9. InetAddress ip = InetAddress.getLoopbackAddress();
  10. // IPv4返回127.0.0.1,IPv6返回::1
  11. System.out.println(ip.getHostAddress());
  12. System.out.println(ip.getHostName());
  13. //获取本地主机IP地址对象
  14. ip = InetAddress.getLocalHost();
  15. System.out.println(ip.getHostAddress()); // 返回IP地址字符串
  16. System.out.println(ip.getHostName()); // 返回IP地址的主机名
  17. }
  18. } /* Output:
  19. 127.0.0.1
  20. localhost
  21. 10.10.7.53
  22. JieHong-PC
  23. *///:~

以上演示的是获取本机的IP地址,当然我们也可以获取其他主机的IP地址,通过主机名或者IP地址字符串。

  1. import java.net.InetAddress;
  2. import java.net.UnknownHostException;
  3. public class IPDemo {
  4. public static void main(String[] args) throws UnknownHostException {
  5. // 在给定主机名(或IP地址字符串)的情况下确定主机的IP地址。
  6. getIPByName();
  7. }
  8. public static void getIPByName() throws UnknownHostException {
  9. InetAddress ip = InetAddress.getByName("JieHong-PC");
  10. System.out.println(ip.getHostAddress());
  11. System.out.println(ip.getHostName());
  12. ip = InetAddress.getByName("10.10.7.53");
  13. System.out.println(ip.getHostAddress());
  14. System.out.println(ip.getHostName());
  15. }
  16. } /* Output:
  17. 10.10.7.53
  18. JieHong-PC
  19. 10.10.7.53
  20. JieHong-PC
  21. *///:~

甚至我们可以通过网站名获取IP地址字符串,以下是一个小展示:

  1. import java.net.InetAddress;
  2. import java.net.UnknownHostException;
  3. public class IPDemo {
  4. public static void main(String[] args) throws UnknownHostException {
  5. hostNameShow();
  6. }
  7. public static void hostNameShow() throws UnknownHostException {
  8. InetAddress ip = InetAddress.getByName("www.sina.com.cn");
  9. System.out.println(ip.getHostAddress());
  10. System.out.println(ip.getHostName());
  11. ip = InetAddress.getByName("www.baidu.com");
  12. System.out.println(ip.getHostAddress());
  13. System.out.println(ip.getHostName());
  14. }
  15. } /* Output:
  16. 202.108.33.98
  17. www.sina.com.cn
  18. 61.135.169.121
  19. www.baidu.com
  20. *///:~

在这里有必要多说一下上面方式的工作原来,"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"。

  1. import java.net.InetAddress;
  2. import java.net.UnknownHostException;
  3. public class IPDemo {
  4. public static void main(String[] args) throws UnknownHostException {
  5. hideWeb();
  6. }
  7. public static void hideWeb() throws UnknownHostException {
  8. InetAddress ip = InetAddress.getByName("www.jiehong.com");
  9. System.out.println(ip.getHostAddress());
  10. System.out.println(ip.getHostName());
  11. ip = InetAddress.getByName("www.game18.com");
  12. System.out.println(ip.getHostAddress());
  13. System.out.println(ip.getHostName());
  14. ip = InetAddress.getByName("192.168.1.100");
  15. System.out.println(ip.getHostAddress());
  16. System.out.println(ip.getHostName());
  17. }
  18. } /* Output:
  19. 127.0.0.1
  20. www.jiehong.com
  21. 192.168.1.100
  22. www.game18.com
  23. 192.168.1.100
  24. www.game18.com
  25. *///:~

如果在这两行中间再添加一行"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。举个例子:你从厦门到鼓浪屿的话,会从你这边海的码头坐船到另一边的码头,这两个码头就相当与Socket。


UDP传输协议

在Java中将UDP这种协议进行了封装,使得方便我们操作。UDP传输协议对应的端点服务Socket为DatagramSocket,此类表示用来发送接收数据报包的套接字。该类的sendreceive方法将从此套接字发送和接收数据报包DatagramPacket表示数据报包,实现无连接包投递服务,不对包投递做出保证。由于所包含的信息太多,例如目的地址和源地址、校验码、包中的数据等,所以将数据封装成对象来方便我们操作。通过查看API可以发现DatagramPacket的构造函数分出两类,一类用来发送数据,一类用来接收数据。注意,发送数据的数据报包都应该明确目的地址,即构造函数中会具有含IP对象的参数。

UDP的发送端

清楚了上面所说的内容,首先我们来明确创建UDP传输协议的发送端的思路:

  1. 建立UDP的Socket服务的套接字即端点;
  2. 将要发送的数据封装到数据报包中;
  3. 通过UPD的Socket服务将数据报包发送出去;
  4. 关闭Socket服务。
  1. import java.io.IOException;
  2. import java.net.DatagramPacket;
  3. import java.net.DatagramSocket;
  4. import java.net.InetAddress;
  5. public class UPDSend {
  6. public static void main(String[] args) throws IOException {
  7. // 构造发送端数据报套接字,并将其绑定到本地主机上任何可用的端口
  8. DatagramSocket ds = new DatagramSocket();
  9. // 将要发送的数据封装到数据报包中
  10. String data = "UPD传输协议:发送端启动";
  11. byte[] buf = data.getBytes();
  12. // 数据报包将发送到指定主机上的指定端口号
  13. DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getByName("JieHong-PC"), 8899);
  14. // 发送数据报包
  15. ds.send(dp);
  16. // 关闭此数据报套接字以释放资源
  17. ds.close();
  18. }
  19. } ///:OK~

UDP的接收端

创建UDP传输协议的接收端的思路:

  1. 建立明确端口号的UDP的Socket服务的套接字;
  2. 创建数据报包,用于存储接收到的数据,方便用数据包对象的方法解析这些数据;
  3. 使用Socket服务的receive方法将接收的数据存储到数据包中;
  4. 通过数据报包的方法解析数据包中的数据;
  5. 关闭Socket服务。
  1. import java.io.IOException;
  2. import java.net.DatagramPacket;
  3. import java.net.DatagramSocket;
  4. public class UDPReceive {
  5. public static void main(String[] args) throws IOException {
  6. // 监听绑定到本地主机上的指定端口,此时与发送的数据报包中的端口一致
  7. DatagramSocket ds = new DatagramSocket(8899);
  8. // 创建数据报包,用于存储接收到的数据
  9. byte[] buf = new byte[1024];
  10. DatagramPacket dp = new DatagramPacket(buf, buf.length);
  11. // 将接收的数据存储到数据包中
  12. ds.receive(dp); // 阻塞式方法
  13. // 解析数据报包的数据
  14. String ip = dp.getAddress().getHostAddress();
  15. int port = dp.getPort(); // 返回发送端绑定的端口号
  16. String data = new String(dp.getData(), 0, dp.getLength()); // 获取接收到的数据的长度
  17. System.out.println(ip + ":" + port + ":" + data);
  18. // 关闭此数据报套接字以释放资源
  19. ds.close();
  20. }
  21. } /* Output: // 只有先启动接收端,后启动发送端才能看到以下输出结果
  22. 10.10.7.53:52684:UPD传输协议:发送端启动
  23. *///:~

通过发送端随机绑定了52684端口,当然我们也可以在发送端的Socket构造函数中指定端口,或者使用bind方法绑定端口。

UDP的应用——聊天室

笔者将演示使用多线程来实现一个聊天室的功能,在主控台输入后显示,当输入886之后退出聊天。

  1. import java.io.BufferedReader;
  2. import java.io.IOException;
  3. import java.io.InputStreamReader;
  4. import java.net.DatagramPacket;
  5. import java.net.DatagramSocket;
  6. import java.net.InetAddress;
  7. // 当发送端发送了886之后便退出聊天
  8. class Send implements Runnable{
  9. private DatagramSocket ds;
  10. public Send(DatagramSocket ds) { this.ds = ds; }
  11. @Override
  12. public void run() {
  13. BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
  14. String line = null;
  15. try {
  16. while ((line = br.readLine()) != null) {
  17. byte[] buf = line.getBytes();
  18. DatagramPacket dp = new DatagramPacket(buf, buf.length, InetAddress.getLocalHost(), 8010);
  19. ds.send(dp);
  20. if ("886".equals(line))
  21. break;
  22. }
  23. } catch (IOException e) { }
  24. }
  25. }
  26. // 接收端
  27. class Receive implements Runnable{
  28. private DatagramSocket ds;
  29. public Receive(DatagramSocket ds) { this.ds = ds; }
  30. @Override
  31. public void run() {
  32. while(true){
  33. byte[] buf = new byte[1024];
  34. DatagramPacket dp = new DatagramPacket(buf, buf.length);
  35. try {
  36. ds.receive(dp); // 阻塞式方法
  37. String ip = dp.getAddress().getHostAddress();
  38. int port = dp.getPort();
  39. String data = new String(dp.getData(), 0, dp.getLength());
  40. System.out.println(ip + ":" + port + ":" + data);
  41. if ("886".equals(data)) {
  42. System.out.println(ip + ":退出聊天室");
  43. }
  44. } catch (IOException e) { }
  45. }
  46. }
  47. }
  48. public class Chat {
  49. public static void main(String[] args) throws IOException {
  50. // 绑定到本地主机上的指定端口
  51. DatagramSocket send = new DatagramSocket(9001);
  52. // 接收端需与发送的数据报包中的端口一致
  53. DatagramSocket receive = new DatagramSocket(8010);
  54. new Thread(new Send(send)).start();
  55. new Thread(new Receive(receive)).start();
  56. }
  57. } ///:0K~
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注