Java从最开始就是支持网络编程的,也正是网络使Java得到发展繁荣。在这里我记录一下如何使用Java进行网络编程,什么是Socket以及Java实现TCP,UDP的编程模型。
InetAddress
InetAddress类位于 java.net 包下,Java的网络编程的大部分类到位于该包下。此类表示互联网协议 (IP) 地址。IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,UDP 和 TCP 协议都是在它的基础上构建的。
InetAddress 的实例包含 IP 地址,还可能包含相应的主机名(取决于它是否用主机名构造或者是否已执行反向主机名解析),InetAddress类没有构造函数,所以不能通过new 的方式构建出来,但是查看API可以发现其某些静态方法可以返回一个InetAddress实例。
如:
- static InetAddress getByAddress(byte[] addr) 在给定原始 IP 地址的情况下,返回 InetAddress 对象。
- static InetAddress getByName(String host) 在给定主机名的情况下确定主机的 IP 地址。
- static InetAddress getLocalHost() 返回本地主机。
其常用的方法有:
- String getHostAddress() 返回 IP 地址字符串(以文本表现形式)。
- String getHostName() 获取此 IP 地址的主机名。
- byte[] getAddress() 返回此 InetAddress 对象的原始 IP 地址。
TCP和UDP
简单的来说就是TCP需要先建立连接通道,是面向连接的可靠的传输层协议,需要先三次握手建立连接,正是因为有连接通道的存在,所以其传输的数据是可靠的。而UDP之间通信不需要建立连接通道,所以其是不可靠的传输层协议,可能出现些许的数据的丢失,而其传输的速度是要快与TCP的,所以在一些对数据完整性要求不高的场景下,如听音乐,看电影等场景,丢失极少部分的数据是不会产生很大影响的,其快速的加载可能才是我们最想要的,所以这时使用UDP较为合适。
URL
URL类也是位于 java.net 包下的。URL类代表一个统一资源定位符,它是指向互联网“资源”的指针。资源可以是简单的文件或目录,也可以是对更为复杂的对象的引用,一个完整的URL可以分为协议://主机名(:端口号)/路径。这个“端口”是可选的,它是用于建立到远程主机 TCP 连接的端口号。如果未指定该端口号,则使用协议默认的端口。例如,HTTP协议的默认端口为 80
。
URLConnection openConnection() //返回一个 URLConnection 对象,它表示到 URL 所引用的远程对象的连接。
我们可以使用这个openConnection方法得到一个URLConnection对象,一般我们使用其子类HttpURLConnection对象,这是一个支持HTTP协议的连接对象,我们可以通过这个连接对象来的得到一些与HTTP有关的信息。
HttpURLConnection
我们可以使用其继承的的 getHeaderFields 方法来得到响应头的信息(以键值对的方式)
可以通过其继承的 getInputStream 方法得到输入流,也就是响应内容
abstract void disconnect() //指示近期服务器不太可能有其他请求,其实也就是断开连接的作用。
接下来通过一个访问百度的例子,得到百度给我们的响应(响应头和响应内容):
package cn.lynu.test; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.List; import java.util.Map; public class HttpConnTest { public static void main(String[] args)throws Exception { //创建URL对象 URL url=new URL("http://www.baidu.com"); //用URL创建HttpURLConnect对象 HttpURLConnection conn =(HttpURLConnection) url.openConnection(); //打开连接 conn.connect(); //打印响应头信息 Map<String, List<String>> header=conn.getHeaderFields(); for (String key:header.keySet()) { System.out.println("key: "+key+" value: "+header.get(key)); } //打印响应内容 BufferedReader br=new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8")); String str=null; while((str=br.readLine())!=null){ System.out.println(str); } conn.disconnect(); //断开连接 } }
Socket
网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端称为一个socket。建立网络通信连接至少要一对socket(服务器的ServiceSocket,客户端的Socket),TCP正是通过Socket在连接通道上进行通信的,而通信的过程其实就是 I/O 的操作,还是我们熟悉的InputStream和OutputStream,接下来我们就来建立TCP的编程模型。
TCP的服务器端
上面说过了TCP需要一对Socket,而存在于服务器端的正是名为ServiceSocket的类,它可以接受客户端的请求,并做出响应,做出响应使用就是OutputStream,而接收请求使用就是InputStream。
接下来我就通过在Socket实现在服务器端接收到客户端的请求之后做出响应
1. 创建ServiceScoket
2.监听客户端的请求
3.通过ServiceSocket的getOutputStream方法得到输出流,通过输出流向客户端做出响应
4.关闭资源
package cn.lynu.test; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; //Java TCP模型 服务端 public class TCPServerA { public static void main(String[] args) throws Exception{ //得到ServerSocket ServerSocket ss=new ServerSocket(8888); //监听客户端发起的请求 Socket s = ss.accept(); //得到输出流(向客户端响应) OutputStream outputStream = s.getOutputStream(); PrintWriter pw=new PrintWriter(outputStream); pw.println("now date: "+new Date()); pw.flush(); pw.close(); s.close(); ss.close(); } }
TCP的客户端
客户端的Socket就叫做Socket,通过Socket可以连接到一个指定的IP地址(InetAddress)的指定端口上(其实也就是一次请求),在上面的例子中服务器有请求到来之后,会做出响应(就是通过输出流输出当前时间) ,客户端可以接收到这个请求并打印在控制台上,使用的就是输入流来接收。
我这里使用Socket来接收服务器端的响应内容,并打印出来
步骤:
1.创建一个Scoket对象
2.使用Socket的getInputStream方法得到输入流,通过输入流得到服务器响应的内容
3.将响应内容打印出来
4.关闭资源
package cn.lynu.test; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.net.Socket; //Java TCP模型 客户端 public class TCPClientA { public static void main(String[] args) throws Exception{ Socket s=new Socket("localhost", 8888); InputStream inputStream = s.getInputStream(); InputStreamReader reader=new InputStreamReader(inputStream); BufferedReader br=new BufferedReader(reader); String line = br.readLine(); //读取一行 System.out.println(line); //打印从服务器端发送的数据 s.close(); } }
这里需要注意的是客户端指定的端口号需要与服务器端指定的端口号一致,这当然是必须的,端口号才是区分不同应用的关键,才能保证接收到的数据准确到达指定的应用。端口号的范围是从0 到65535,但是0~1023是系统保留的端口号,我们不要去使用。
运行的时候我们要先运行服务器端的程序,再运行客户端的程序。
使用多线程的TCP服务器端
在上面的TCP服务器端模型中,一个服务器端只能与一个客户端建立连接,想要一个服务器可以与多个客户端建立连接就需要使用多线程了。思路就是在每一个客户端请求到来时,就新建一个线程用于给其响应,而不是之前的main线程中响应,在main线程中做响应可不就是一个线程(一个main线程)只能处理一个请求嘛,主线程运行完了也不会再接收其他请求了,所以多线程将每一个请求响应与每一个线程绑定,就可以服务多个客户端。
package cn.lynu.test; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; import java.util.Date; /** * TCP服务器端多线程并发模型 * @author LZ * */ public class TCPServerB { public static void main(String[] args)throws Exception { //创建serverSocket对象,监听端口 ServerSocket ss=new ServerSocket(8888); //开始循环监听请求 Socket socket=null; while((socket=ss.accept())!=null){ //开启一个新线程 new MyThread(socket).start(); } ss.close(); } } //线程类 class MyThread extends Thread{ private Socket socket; public MyThread(Socket socket) { this.socket=socket; } @Override public void run() { try { OutputStream outputStream = socket.getOutputStream(); //得到输出流 PrintWriter pw=new PrintWriter(outputStream); pw.write("now date: "+new Date()); pw.flush(); socket.close(); } catch (Exception e) { e.printStackTrace(); } } }
在run方法中就是给每个客户端做出响应,只不过这里比较简单,只是给每个客户端都响应相同的一件事(输出当前时间)
UDP服务器端
UDP使用 Datagram (数据报)来作为传输的数据格式的,将数据和IP地址端口号等信息封装进数据报中。在Java中使用DatagramPacket类来表示数据报,并通过DatagramSocket类来完成请求或响应,这与使用流的方式的TCP模型有很大的不同。
这里我使用服务器端来接收客户端的请求,并将请求数据打印出来
步骤:
1.创建DatagramSocket对象,并指定端口号
2.使用一个字节数组
3.通过这个字节数组来创建一个DatagramPacket 数据报包对象
4.通过DatagramSocket的receive方法来接收数据并放入DatagramPacket 中
5.显示请求数据(Datagrampacket的getData方法)
6.关闭资源
package cn.lynu.test; import java.net.DatagramPacket; import java.net.DatagramSocket; //Java UDP模型 服务器端(接收端) public class UDPServerA { public static void main(String[] args) throws Exception{ //创建DatagramSocket对象,并指定端口号 DatagramSocket ds=new DatagramSocket(9999); //一个字节数组接受数据 byte[] buffer=new byte[1024]; //创建数据包对象 DatagramPacket dp=new DatagramPacket(buffer, buffer.length); ds.receive(dp); //接收数据 //得到数据DatagramSocket的getData方法 String string=new String(dp.getData(), 0, dp.getLength()); System.out.println(string); //打印接收的结果 ds.close(); } }
UDP的客户端模型
我这里使用客户端向服务器端发送请求数据
步骤:
1.创建DatagramSocket对象,并指明客户端发送发端口号,可以与服务器端的接收方端口号不一致,因为这里只是在说是谁发送了数据,而真正指明数据在哪里的端口是封装在数据报中的
2.创建请求数据(这里也就是一个字符串),并将数据,IP地址,端口号(这时的端口号一定要与服务器端接收方的端口号一致)封装成一个DatagramPacket
3.通过DatagramSocket的send方法来发送数据
4.关闭资源
package cn.lynu.test; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; //Java UDP模型 客户端端(发送端) public class UDPClientA { public static void main(String[] args)throws Exception { //UDP发送端和接收端口可以不一致 DatagramSocket ds=new DatagramSocket(9998); String abc="abc"; //但是创建数据包时指定的端口号需要与接收端一致 DatagramPacket dp=new DatagramPacket(abc.getBytes(), 0,abc.length(),InetAddress.getByName("localhost"),9999); //发送数据 ds.send(dp); ds.close(); } }
使用TCP网络编程模型来写一个简单的web服务器
其实就是在多线程服务器端的例子上改的,将run中的响应换成了一段HTML,并将端口号换成了80端口,HTTP默认使用80端口
package cn.lynu.test; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; /** * TCP 模型 构建简易web服务器 * @author LZ * */ public class WebServer { public static void main(String[] args) throws Exception{ //使用ServerSocket对象监听端口,Web端口默认使用80 //如何修改端口号,可以在浏览器中要使用类似:http://localhost:8888/ ServerSocket ss=new ServerSocket(80); Socket socket=null; while((socket=ss.accept())!=null){ new HttpThread(socket).start(); } socket.close(); } } //线程类 处理HTTP class HttpThread extends Thread{ private Socket socket; public HttpThread(Socket socket){ this.socket=socket; } @Override public void run() { try { OutputStream outputStream = socket.getOutputStream(); PrintWriter pw=new PrintWriter(outputStream); pw.println("<html>"); pw.println("<body>"); pw.print("This is my page"); pw.println("</body>"); pw.println("</html>"); pw.flush(); socket.close(); } catch (IOException e) { e.printStackTrace(); } } }