网络编程
java提供了网络通信的支持,通过这些支持,java程序可以非常方便的访问互联网上的HTTP服务,FTP服务等,并可以直接取得互联网上的远程资源,还可以向远程资源发送GET,POST请求。
网络基础知识
计算机网络是现代通信技术与计算机技术相结合的产物,计算机网络可以提供以下主要功能。
- 资源共享
- 信息传输与集中处理
- 均衡负荷与分布处理
- 综合信息服务
计算机网络通常是按照规模大小和延伸范围来分类的,常见的分类:局域网(LAN),城域网(MAN),广域网(WAN)。Internet可以视为世界上最大的广域网。
按照网络拓扑图结构来划分,可以分为星型网络,总线型网络,环形网络,树形网络等。
按照传出介质来划分,可分为:双绞网线,同轴电缆网,光纤网,卫星网等。
计算机网络通信必须有一些约定,这些约定被称为通信协议。通信协议:负责对传输速率,传输代码,代码结构,传输控制步骤,出错控制,等制定了处理标准。
通信协议通常由三部分组成,一是语义部分,用于决定双方对话的类型;二是语法部分,用于决定双方对话的格式;三是变换规则,用于决定通信双方的答应关系。
国际标准化组织ISO提出了“开放系统互连参考模型”,即OSI。该模式力求将网络简化,并以模块坏的方式设计网络。
开放系统互连参考模型把计算机网络分成物理层,数据链路层,网络层,传输层,会话层,表示层,应用层七层,:
IP协议又称互联网协议,是支持网间互连的数据报协议。它提供网间连接的完善功能,包括IP数据报规定互联网范围内的地址格式。
TCP协议即传输控制协议,它规定了一种可靠的数据信息专递服务。
TCP/IP协议是在同一时期作为一个协议来设计的,并且在功能上也是互补的。因此TCP/IP协议也是Internet中最常见的基础协议。按照TCP/IP协议模型,网络通常被分为一个四层模型,对应OSI如下图:
IP地址和端口号
IP地址用于唯一标识网络中的一个通信实体,这个通信实体即可以是一台电脑,一台打印机,路由器某个端口。而在基于IP协议网络中传输数据包,都必须使用IP地址来进行标识。
Ip地址是数字型的,IP地址是一个32位整数,通常便于记忆,把它分成4个8为的二进制数,每8位之间用圆点隔开,每个8位整数都可以转换成0-255的十进制整数,因此常见的IP地址如:192.168.1.1
NIC统一负责全球Internet Ip地址的规划,管理,而Inter NIC,APNIC,RIPE三大网络信息中心具体负责美国及其他地区IP地址分配。其中APNIC负责亚太地区的IP管理,我国申请的IP地址也要通过APNIC,APNIC的总部设在日本东京大学。
IP地址分为A,B,C,D,E 五类,每个类别的网络标识和主机标识各有规则
A类:10.0.0.0-10.255.255.255
B类:172.16.0.0-172.31.255.255
C类:192.168.0.0-192.168.255.255
IP地址用于唯一地标识网络上的一个通讯实体,但一个通讯实体可以有多个通信 同时提供网络服务,此时还需要使用端口。
端口是一个16位的整数,用于表示数据交给哪个通讯程序处理,因此,端口就是应用程序与外界交流的出入口,他是一种抽象的软件结构,包括一些数据结构和IO缓冲区。
不同的应用程序处理不同端口上的数据,同一台机器上不能有两个程序使用同一个端口,端口号可以从0到65535,分为三类:
公认端口:从0到1023,他们紧密绑定一些特定的服务
注册端口:从1024到49151,他们松散绑定一些服务,应用程序通常应该使用这个范围内的端口
动态和私有端口:从49152到65535,这些端口是应用程序使用的动态端口,应用程序一般不会主动使用这些端口
当一个程序需要发送数据时,需要指定目的地的IP地址和端口,如果指定了正确的IP地址和端口号,计算机网络就可以将数据送给该IP地址和端口所对应的程序。
java的基本网络支持
java为网络支持提供了java.net包,该包下的URL和URLConnection等类提供了 以编程方式访问web服务的功能,而URLDecoder和URLEncoder则提供了普通字符串和application/x-www-form-urlencoded MIME字符串相互转换的静态方法。
使用InetAddress
java提供了InetAddress类来代表Ip地址,该类下有两个子类:Inet4Addrss和Inet6Address分别代表 IPv4地址和IPv6地址。
InetAddress类没有提供构造器,而是提供静态方法来获取实例:
getByName(String host):根据主机获取对应的InetAddress对象
getByAddress(byte[] addr):根据原始IP地址来获取对应的InetAddress对象
InetAddress还提供了三个方法获取实例对象的IP地址和主机名:
String getCanonicalHostName():获取此IP地址的全限定域名
String getHostAddress():返回该InetAddress实例对应的IP地址字符串
String getHostName():返回此IP地址的主机名
InetAddress getLocalHost():获取本机IP地址对应的InetAddress实例
boolean isReachable():测试是否可以到达该地址,该尽力通过实施作出努力到达主机,但防火墙和服务器配置可能会阻止导致无法访问的状态,而一些特定的端口,可访问请求。 一个典型的实现将使用ICMP echo请求,如果能获得特权,否则将尝试建立目标主机的端口TCP连接7
//实例
public class InetAddressTest {
public static void main(String[] args) throws Exception {
//根据主机名来获取对应的InetAddress实例
InetAddress ip = InetAddress.getByName("www.baidu.com");
//判断是否可达
System.out.println("baidu是否可达:"+ip.isReachable(2000));
//获取该InetAddress实例的IP字符串
System.out.println(ip.getHostAddress());
//根据原始Ip来获取对应的InetAddress实例
InetAddress local = InetAddress.getByAddress(new byte[]{127, 0, 0, 1});
System.out.println("本机是否可达:"+local.isReachable(5000));
//获取该InetAddress实例对应的全限定域名
System.out.println(local.getCanonicalHostName());
}
}
使用URLDecoder和URLEncoder
URLDecoder和URLEncoder用于完成普通字符串和application/x-www-form-urlencoded MIME 字符串之间的相互转换。
上图搜索中,关键字包含中文,这些关键字变成了乱码,—实际上这不是乱码,而是application/x-www-form-urlencoded MIME字符串,当URL地址里包含了非西欧字符的字符串时,系统会将这些非西欧字符串转换成如上图的特殊字符串。
编程中可能涉及普通字符串和这种字特殊字符串的相关转换,这时就需要使用URLDecoder和URLEncoder类。
//
public class URLDecoderTest {
public static void main(String[] args) throws Exception {
//将application/x-www-form-urlencoded MIME字符串
//转换成普通字符串
//其中的字符串直接从图中所示复制过来
String keyWord= URLDecoder.decode("%E7%96%AF%E7%8B%82java","utf-8");
System.out.println(keyWord);
//将普通字符串转换
//application/x-www-form-urlencoded MIME字符串
String urlStr= URLEncoder.encode("疯狂Android讲义","GBK");
System.out.println(urlStr);
}
}
URL,URLConnection和URLPermission
URL对象代表统一资源定位器,它指的是互联网 资源的指针。资源可以是简单的文件或目录,也可以是对更为复杂对象的引用。通常情况下,URL可以有协议名,主机,端口,和资源组成,
protocol://host:port/resourceName
如假设:http://www.bbcxx.com/index.php
URL 提供了多个构造器用于创建URL对象,一旦获得URL对象,就可以使用下面方法:
URL对象还提供openStream()方法读取该URL资源的InputStream,通过该方法可以非常方便的读取远程资源----甚至实现多线程下载。
//实现一个多线程下载工具类
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Random;
public class DownUtil {
//定义下载资源的路径
private String path;
//指定下载的文件保存位置
private String targetFile;
//定义需要多少个线程下载资源
private int threadNum;
//定义下载的线程对象
private DownThread[] threads;
//定义下载的文件总大小
private int fileSize;
public DownUtil(String path,String targetFile,int threadNum){
this.path=path;
this.threadNum=threadNum;
//初始化threads数组
threads=new DownThread[threadNum];
this.targetFile=targetFile;
}
public void download() throws Exception {
URL url=new URL(path);
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5*1000);
conn.setRequestMethod("GET");
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg"
+"application/x-shockwave-flash, application/xaml+xml,"
+"application/vnd.ms-xpsdcument, application/x-ms-xbap,"
+"application/x-ms-application, application/vnd.ms-excel, "
+"application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language","zh-CN");
conn.setRequestProperty("Charset","UTF-8");
//得到文件大小
fileSize=conn.getContentLength();
int currentPartSize=fileSize/threadNum+1;
RandomAccessFile file=new RandomAccessFile(targetFile,"rw");
//设置本地文件的大小
file.setLength(fileSize);
file.close();
for (int i = 0; i < threadNum; i++) {
//计算每个线程下载的开始位置
int startPos=i*currentPartSize;
//每个线程使用一个RandomAccessFile进行下载
RandomAccessFile currentPart=new RandomAccessFile(targetFile,"rw");
//定位该线程的下载位置
currentPart.seek(startPos);
//创建下载线程
threads[i]=new DownThread(startPos,currentPartSize,currentPart);
//启动下载线程
threads[i].start();
}
}
//获取下载完成百分比
public double getCompletRate(){
//统计多个线程已经下载的总大小
int sumSize=0;
for (int i = 0; i < threadNum; i++) {
sumSize+=threads[i].length;
}
//返回已经完成的百分比
return sumSize*1.0/fileSize;
}
//内部类DownThread
private class DownThread extends Thread{
//当前线程的下载位置
private int startPos;
//定义当线程下载文件的大小
private int currentPartSize;
//当线程需要下载的文件块
private RandomAccessFile currentPart;
//定义线程已下载的字节数
public int length;
public DownThread(int startPos,int currentPartSize,RandomAccessFile currentPart){
this.startPos=startPos;
this.currentPartSize=currentPartSize;
this.currentPart=currentPart;
}
@Override
public void run() {
try{
URL url=new URL(path);
HttpURLConnection conn= (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5*1000);
conn.setRequestMethod("GET");
conn.setRequestProperty(
"Accept",
"image/gif, image/jpeg, image/pjpeg, image/pjpeg"
+"application/x-shockwave-flash, application/xaml+xml,"
+"application/vnd.ms-xpsdcument, application/x-ms-xbap,"
+"application/x-ms-application, application/vnd.ms-excel, "
+"application/vnd.ms-powerpoint, application/msword, */*");
conn.setRequestProperty("Accept-Language","zh-CN");
conn.setRequestProperty("Charset","UTF-8");
InputStream inStream=conn.getInputStream();
//跳过startPos个字节,表名该线程只下载自己负责的那部分文件
inStream.skip(this.startPos);
byte[] buffer=new byte[1024];
int hasRead=0;
//读取网络数据,并写入本地文件
while (length<currentPartSize&&(hasRead=inStream.read(buffer))!=-1){
currentPart.write(buffer,0,hasRead);
//累计该线程下载的总大小
length+=hasRead;
}
currentPart.close();
inStream.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}
上面程序中定义DownThread线程类,该线程负责读取从start开始到end结束的所有字节数据,并写入到RandomAccessFile对象中,run()方法中实现了一个简单的输入,输出实现。
程序中DownUtil类中的download()方法负责按如下步骤实现多线程下载:
- 创建URL对象
- 获取指定URL对象所指向资源的大小(通过getContentLength()方法获取),此处用到了URLConnection类,该类代表java应用程序和URL之间的通信链接。
- 从本地磁盘上创建一个与网络资源具有相同大小的空文件
- 计算每个线程应该下载网络资源的哪个部分(从哪个字节开始,哪个字节结束)
- 依次创建,启动多个线程来下载网络资源的指定部分
下面在主程序调用工具类的down()方法执行下载
public class MultiThreadDown {
public static void main(String[] args) throws Exception {
//初始化DownUtil对象
final DownUtil downUtil=new DownUtil(
"https://img-blog.csdnimg.cn/2020091714452670.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NzZG5uZXdz,size_16,color_FFFFFF,t_70#pic_center"
,"j.png",4);
//开始下载
downUtil.download();
new Thread(()->{
while(downUtil.getCompletRate()<1){
//每隔0.1秒查询一次任务完成进度
//GUI程序中可根据进度来绘制进度条
System.out.println("已完成:"+downUtil.getCompletRate());
try{
Thread.sleep(1000);
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
运行上面程序得到一张图片j.png,该程序要用到了URLConnection和HttpURLConnection对象,其中前者表示应用程序和URL之间的通信链接,后者表示URL之间的Http连接,程序可以通过URLConnection实例向URL发送请求,读取URL引用的资源
java8新增了一个URLPermission工具类,用于管理HttpURLConnection的权限问题,如果在HttpURLConnection安装了安全管理器,通过该对象打开连接时就需要获得权限。
通常创建一个和URL的连接,并发送请求,读取此URL引用的资源需要如下几步:
- 通过调用URL对象的openConnection()方法创建URLConnection对象
- 设置URLConnection的参数和普通请求属性
- 如果只是发送GET方式请求,则使用connect()方法建立和远程资源之间的实际连接即可;如果需要发送POST方式的请求,则需要获取URLConnection实例对应的输出流来发送请求
- 远程资源变为可用,程序可以访问远程资源的头字段或通过输入流读取远程资源的数据。
如果既要使用输入流读取URLConnection响应的内容,又要使用输出流发送请求参数,则一定先使用输出流,在使用输入流
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
public class GetPostTest {
/**
* 向指定URL发送GET请求
* @param url 发送请求的URL
* @param param 请求参数,格式满足name1=value1&name2=value2的形式
* @return URL代表远程资源的响应
*/
public static String sendGet(String url,String param){
String result="";
String urlName=url+"?"+param;
try {
URL realUrl = new URL(urlName);
//打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connecton", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
//建立市级连接
conn.connect();
//获取所有响应头字段
Map<String, List<String>> map = conn.getHeaderFields();
//遍历所有的响应头
for (String key : map.keySet()) {
System.out.println(key + "--->"+map.get(key));
}
try (//定义BufferedReader输入流来读取URL的响应
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8"))) {
String line;
while ((line = in.readLine()) != null) {
result += "\n" + line;
}
}
}catch (Exception e) {
e.printStackTrace();
}
return result;
}
public static String sendPost(String url,String param){
String result="";
try{
URL realUrl=new URL(url);
//打开和URL之间的连接
URLConnection conn = realUrl.openConnection();
//设置通用的请求属性
conn.setRequestProperty("accept", "*/*");
conn.setRequestProperty("connecton", "Keep-Alive");
conn.setRequestProperty("user-agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
//发送POST请求必须设置如下两行
conn.setDoOutput(true);
conn.setDoInput(true);
try(//获取URLConnetion对象对应的输出流
PrintWriter out=new PrintWriter(conn.getOutputStream())){
//发送请求参数
out.print(param);
//flush输出流的缓冲
out.flush();
}
try(//BufferedReader输入流来读取URL的响应
BufferedReader in=new BufferedReader(new InputStreamReader(conn.getInputStream()))){
String line;
while((line=in.readLine())!=null){
result+="\n"+line;
}
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
//提供主方法,测试GET和POST请求
public static void main(String[] args) {
//发送GET请求
String s=GetPostTest.sendGet("https://www.baidu.com",null);
System.out.println(s);
//发送POST请求
String s1=GetPostTest.sendPost("https://www.baidu.com","wd=nihao");
System.out.println(s1);
}
}
不管是发送GET请求还是POST请求,程序获取URLConnection响应的方式是一样的,如果程序可以确定远程响应是字符流,则可以使用字符流来读取,如果程序无法确定远程响应是字符流,则使用字节流读取即可。
基于TCP协议的网络编程
TCP/IP通信协议是一种可靠的网络协议,他在 通过的两端建立一个Socket,从而在通信的两端之间形成网络虚拟链路。一旦创建了网络虚拟链路,两端的程序就可以通过虚拟链路进行通信。java对基于TCP协议的网络通信提供了良好的封装,java使用Socket对象来代表两端的通信端口,并通过Socket产生IO流进行网络通信。
TCP协议基础
TCP协议被称作一种端对端协议。这是因为它对两台计算机之间的连接起了重要作用----当一台计算机需要与另一台计算机连接时,TCP协议让它们建立了一个连接:用于发送和接收数据的虚拟链路
TCP协议负责收集这些信息包,并将其按适当的次序放好传送,接收端收到后将其正确地还原。此协议保证了数据包在传输中准确无误。TCP协议使用了重发机制–当一个通讯实体发送一个消给另一个通讯实体后,需要收到另一个通讯实体的确认消息,如果没有收到,则会再次重发刚才的信息。
通过这种重发机制,TCP协议向应用程序提供了可靠的通信链路,使它能够自适应网上各种变化。即使Internet暂时出现阻塞的情况下,TCP也能够保证通信的可靠性
使用ServerSocket创建TCP服务器端
java能够接收其他通信实体连接请求的类是ServerSocket,ServerSocket对象用于监听来自客户端的Socket连接,如果没有连接,它将一直处于等待状态。ServerSocket包含了一个来自客户端连接请求的方法:
当ServerSocket使用完毕后,应该使用ServerSocket的close()方法关闭ServerSocket。通常情况下,服务器不应该只接收一个客户端请求,而是不断地接收来自客户端的所有请求
使用Socket进行通信
客户端通常可以使用Socket的构造器来连接到指定的服务器,Socket通常可以使用如下两个构造器:
这两个构造器中指定远程主机即可使用InetAddress来指定,也可以直接使用String对象来指定,但程序通常使用String对象来指定远程IP地址。
当客户端,服务端产生了对应的Socket之后,程序无须在区分服务端,客户端,而是通过各自的Socket进行通信。Socket提供两个方法获取输入流和输出流:
//实例,简单服务器端
public class Server {
public static void main(String[] args) throws IOException {
//创建一个ServerSocket,用于监视客户端Socket的连接请求
ServerSocket ss=new ServerSocket(1000);
//采用循环不断地接收来自客户端请求
while(true){
//每当接收到客户端Socket的请求时,服务器端也对应产生一个Socket
Socket s=ss.accept();
//将Socket对应的输出流包装成PrintStream
PrintStream ps=new PrintStream(s.getOutputStream());
//进行普通IO操作
ps.println("您好,您收到了服务器的新年祝福");
//关闭输出流,关闭Socket
ps.close();
s.close();
}
}
}
使用Socket建立与指定IP地址,指定端口的连接,并使用Socket获取输入流读取数据
//简单的客户端
public class Client {
public static void main(String[] args) throws IOException {
Socket socket=new Socket("192.168.1.1",1000);
//将Socket对应的输入流包装成BufferedReader
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//进行普通IO操作
String line=br.readLine();
System.out.println("来自服务器的数据:"+line);
//关闭输入流,关闭Socket
br.close();
socket.close();
}
}
通过程序可以看出,一旦使用ServerSocket,Socket建立网络连接后,程序通过网络通信与普通IO并没有太大区别。
在实际开发中,程序可能不想让执行网络连接,读取服务器数据进程一直阻塞,而是希望当网络连接,读取操作超过合理时间后,系统自动认为该操作失败,这个合理时间就是超时时长。
Socket对象提供了一个setSoTimeout(int timeout)方法来设置超时时长。
Socket s=new Socket("127.0.0.1",1000);
s.setSoTimeout(10000);
//当指定了超时时长之后,如果使用Socket进行读写操作完成之前超出了该时间限制,那么会抛出异常
假设需要为Socket连接服务器指定超时时长,即经过指定时间后,如果该Socket还未连接到远程服务器,则系统认为该Socket连接超时。但Socket的所有构造器里还没有提供指定的超时时长的参数,所以程序应该先创建一个无连接的Socket,在调用Socket的connect()方法来连接远程服务器,而connect()方法就可以接收一个超时时长
//创建一个无连接的Socket
Socket s=new Socket();
//让该Socket连接到远程服务器,如果经过10秒还没有连接上,则认为连接超时
s.connect(new InetAddress(host,port),10000);
加入多线程
使用传统的BufferedReader的readLine()方法读取数据时,在该方法成功返回之前,线程被阻塞,程序无法继续执行。考虑到这个原因,服务器端应该为每个Socket单独开启一个线程,每个线程负责与一个客户端进行通信。
客户端读取服务端数据的线程同样会被阻塞,所以系统应该单独启动一个线程,该线程专门负责读取服务器数据。
下面实现服务器端的实现代码,程序为服务器端提供两大类,一个是创建ServerSocket监听的主类,一个是负责处理每个Socket通信的线程类。
//客户端类
public class MyServer {
//定义保存所有Socket的ArrayList,并将其包装成线程安全的
public static List<Socket> socketList= Collections.synchronizedList(new ArrayList<>());
public static void main(String[] args) throws IOException {
ServerSocket ss=new ServerSocket(30000);
while(true){
//此行代码会阻塞,将一直等待别人连接
Socket s=ss.accept();
socketList.add(s);
//每当客户端连接后启动一个ServerSocket线程为该客户顿服务
new Thread(new ServerThread(s)).start();
}
}
}
//服务器端线程类
public class ServerThread implements Runnable {
//定义当前线程所处理的Socket
Socket s=null;
//该线程所处理的Socket对应的输入流
BufferedReader br=null;
public ServerThread(Socket s) throws IOException {
this.s=s;
//初始化Socket对应的输入流
br=new BufferedReader(new InputStreamReader(s.getInputStream()));
}
@Override
public void run() {
try{
String content=null;
//才用循环不断从Socket中读取客户顿发过来的数据
while((content=readFromClient())!=null){
//遍历socketList中的每一个Socket
//将读到的内容向每个Socket发送一次
for (Socket s : MyServer.socketList) {
PrintStream ps=new PrintStream(s.getOutputStream());
ps.println(content);
}
}
}catch (IOException e){
e.printStackTrace();
}
}
public String readFromClient(){
try{
return br.readLine();
}catch (IOException e){
//删除该Socket
MyServer.socketList.remove(s);
}
return null;
}
}
上面的服务器端线程类不断读取客户顿数据,程序使用readFromClient()方法来读取客户顿数据,如果读取数据过程中捕获到IOExection异常,则表明该Socket对应的客户顿Socket出现了问题,程序就将该Socket从socketList集合中删除
当服务器端线程读到客户端数据之后,程序遍历socketList集合,并且该数据向socketList集合中的每个Socket发送一次----该服务器端线程把从Socket中读到的数据向socketLIst集合中的每个Socket转发一次。
每个客户端应该包含两个线程,一个负责读取用户的键盘输入,并将用户输入的数据写入Socket对应的输出流中;一个负责读取Socket对应的输入流中的数据,并将这些数据打印出来。
//客户端主程序
public class MyClient {
public static void main(String[] args) throws Exception {
Socket s=new Socket("127.0.0.1",10001);
//客户顿启动ClientThread线程不断地读取来自服务器端的数据
new Thread(new ClientThread(s)).start();
//获取该Socket对应的输出流
PrintStream ps=new PrintStream(s.getOutputStream());
String line=null;
//不断读取键盘输入
BufferedReader br=new BufferedReader(new InputStreamReader(s.getInputStream()));
while((line=br.readLine())!=null){
//将用户的键盘输入内容写入Socket对应的输出流
ps.println(line);
}
}
}
//客户端线程代码
public class ClientThread implements Runnable {
//该线程负责处理的Socket
private Socket s;
//该线程所处理的Socket对应的输入流
BufferedReader br=null;
public ClientThread(Socket s) throws IOException {
this.s=s;
br=new BufferedReader(new InputStreamReader(s.getInputStream()));
}
@Override
public void run() {
try{
String content=null;
//不断地读取Socket输入流中的内容,并将这些内容打印输出
while((content=br.readLine())!=null){
System.out.println(content);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
记录用户信息
当一个客户端信息发送给服务器端之后,服务器端必须可以判断该信息到底是向所有用户发送,还是向指定用户发送,并需要知道向哪个用户发送,这里需要解决两个问题:
- 客户端发送来的信息必须有特殊的标识—让服务器端可以判断是公聊信息,还是私聊信息
- 如果是私聊信息,客户端会发送该消息的目的用户给服务器端,服务器端如何将该信息发送给该私聊用户
解决第一个问题,可以让客户端在发送不同信息之前,先对这些信息进行适当处理—这些特殊字符被称为协议字符。
解决第二个问题,可以考虑使用Map来保存聊天室所有用户和对应Socket之间的映射关系–这样服务器端就可以根据用户名来找到对应的Socket。但本程序仅仅是用Map保存了聊天室所有的用户名和对应输出流之间的映射关系,因为服务器端只获取该用户名对应的输出流即可。
//服务器端提供了一个HashMap的子类,该类不允许value重复,并提供根据value获取key,根据key删除value等方法
public class CrazyitMap<K,V> {
//创建一个线程安全的HashMap
public Map<K,V> map= Collections.synchronizedMap(new HashMap<K,V>());
//根据value来删除指定项
public synchronized void removeByValue(Object value){
for (Object key : map.keySet()) {
if(map.get(key)==value){
map.remove(key);
break;
}
}
}
//获取所有value组成的Set集合
public synchronized Set<V> valueSet(){
Set<V> result=new HashSet<V>();
//将map中所有的value添加到result集合中
map.forEach((key,value)->result.add(value));
return result;
}
//根据value查找key
public synchronized K getKeyByValue(V val){
//遍历所有key组合的集合
for (K key : map.keySet()) {
//如果指定key对应的value与被搜索的value相同,则返回对应的key
if(map.get(key)==val||map.get(key).equals(val)){
return key;
}
}
return null;
}
//实现put()方法,该方法不允许value重复
public synchronized V put(K key,V value){
//遍历所有的value组合的集合
for (V val : valueSet()) {
//如果某个value与试图放入集合的value相同
//则抛出一个RunntimeException异常
if(val.equals(value)&&val.hashCode()==value.hashCode()){
throw new RuntimeException("MyMap实例中不允许出现重复value");
}
}
return map.put(key,value);
}
}
严格来说,CrazyitMap已经不是一个标准的Map结构了,但程序需要这样一个数据结构来保存用户名和对应输出流之间的映射关系,这样既可以通过用户名找到对应输出流,也可以根据输出流找到对应的用户名。
//服务器端d额主类一样是建立ServerSocket来监听来自客户端Socket的连接请求,但该程序增加了一些异常处理
public class Server {
private static final int SERVER_PORT=30000;
//使用CrazyitMap对象来保存每个客户名字和对应的输出流之间的对应关系
public static CrazyitMap<String, PrintStream> clients=new CrazyitMap<>();
public void init(){
try(
//建立监听的ServerSocket
ServerSocket ss=new ServerSocket(SERVER_PORT);
){
//才用死循环来不断地接收来自客户端的请求
while(true){
Socket socket=ss.accept();
new ServerThread(socket).start();
}
}
//如果抛出异常
catch (IOException e){
System.out.println("服务器启动失败,是否端口"+SERVER_PORT+"被占用");
}
}
public static void main(String[] args) {
Server server=new Server();
server.init();
}
}
上面代码完成建立ServerSocket,监听客户端Scoket连接请求,并为已连接的Socket启动单独的线程
//该线程类分别处理公聊,私聊 两类聊天信息,除此还处理用户名是否重复的问题
public class ServerThread extends Thread {
private Socket socket;
BufferedReader br=null;
PrintStream ps=null;
//定义一个构造器,用于接收一个Socket来创建ServerThread线程
public ServerThread(Socket socket){
this.socket=socket;
}
@Override
public void run() {
try{
//获取该Socket对应的输入流
br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//获取该Socket对应的输出流
ps=new PrintStream(socket.getOutputStream());
String line=null;
while ((line=br.readLine())!=null){
//如果读到行以CrazyitProtocol.USER_ROUND开始,并以其结束
//则可以确定读到是用户登陆的用户名
if(line.startsWith(CrazyitProtocol.USER_ROUND)&&line.endsWith(CrazyitProtocol.USER_ROUND)){
//得到真实消息
String userName=getRealMsg(line);
//如果用户名重复
if(Server.clients.map.containsKey(userName)){
System.out.println("重复");
ps.println(CrazyitProtocol.NAME_REP);
}else{
System.out.println("成功");
ps.println(CrazyitProtocol.LOGIN_SUCCESS);
Server.clients.put(userName,ps);
}
}else if(line.startsWith(CrazyitProtocol.PRIVATR_ROUND)&&line.endsWith(CrazyitProtocol.PRIVATR_ROUND)){
//得到真实消息
String msg=getRealMsg(line);
//遍历clients中的每个输出流
for (PrintStream clientPs : Server.clients.valueSet()) {
clientPs.println(Server.clients.getKeyByValue(ps)+"说"+msg);
}
}
}
}
//捕获到异常后,表明该Socket对应的客户顿已经出现了问题
//所以程序将其对应的输出流从Map中删除
catch (IOException e){
Server.clients.removeByValue(ps);
System.out.println(Server.clients.map.size());
//关闭网络,IO资源
try{
if(br!=null){
br.close();
}
if(ps!=null){
ps.close();
}
if(socket!=null){
socket.close();
}
}catch (IOException e1){
e1.printStackTrace();
}
}
}
private String getRealMsg(String line){
return line.substring(CrazyitProtocol.PROTOCOL_LEN,line.length()-CrazyitProtocol.PROTOCOL_LEN);
}
}
上面除了增加了异常处理之外,主要增加了对读取数据的判断,程序读取到客户端发过来的内容后,会根据内容前后的协议字符串对该内容进行相应的处理。
//客户端主类增加了让用户输入用户名的代码,并且不允许用户名重复,除此之外
//还可以根据用户名的键盘输入来判断用户是否想发生私聊消息
public class Client {
private static final int SERVER_PORT=30000;
private Socket socket;
private PrintStream ps;
private BufferedReader brServer;
private BufferedReader keyIn;
public void init(){
try{
//初始化代表键盘的输入流
keyIn=new BufferedReader(new InputStreamReader(System.in));
//连接到服务器端
socket=new Socket("127.0.0.1",30000);
//获取该Socket对应的输入流和输出流
ps=new PrintStream(socket.getOutputStream());
brServer=new BufferedReader(new InputStreamReader(socket.getInputStream()));
String tip="";
//才用循环不断地弹出对话框要求输入用户名
while (true){
String userName= JOptionPane.showInputDialog(tip+"输入用户名");
//在用户输入用户名前缀后缀增加协议字符串后发送
ps.println(CrazyitProtocol.USER_ROUND+userName+CrazyitProtocol.USER_ROUND);
//读取服务器端的响应
String result=brServer.readLine();
//如果用户名重复,则开始下次循环
if(result.equals(CrazyitProtocol.NAME_REP)){
tip="用户名重复,请重新";
continue;
}
//如果服务器端成功返回登陆成功,则循环结束
if(result.equals(CrazyitProtocol.LOGIN_SUCCESS)){
break;
}
}
}catch (UnknownHostException ex){
System.out.println("找不到远程服务器,请确定服务器已启动");
closeRs();
System.exit(1);
}catch (IOException ex){
System.out.println("网络异常!重新登陆");
closeRs();
System.exit(1);
}
//以该Socket对应的输入流启动ClientThread线程
new ClientThread(brServer).start();
}
//定义一个键盘输出,并向网络发生的方法
private void readAndSend(){
try{
//不断读取键盘输入
String line=null;
while ((line=keyIn.readLine())!=null){
//如果发送的信息中没有冒号,且以//开头,则认为想发送私聊信息
if(line.indexOf(":")>0&&line.startsWith("//")){
line=line.substring(2);
ps.println(CrazyitProtocol.PRIVATR_ROUND+line.split(":")[1]+CrazyitProtocol.PRIVATR_ROUND);
}else {
ps.println(CrazyitProtocol.MSG_ROUND+line+CrazyitProtocol.MSG_ROUND);
}
}
}catch (IOException ex){
System.out.println("网络通信异常,请重新登陆");
closeRs();
System.exit(1);
}
}
//关闭Socket,输入流,输出流方法
private void closeRs() {
try{
if (keyIn!=null){
ps.close();
}
if(brServer!=null){
ps.close();
}
if(ps!=null){
ps.close();
}
if(socket!=null){
keyIn.close();
}
}catch (IOException ex){
ex.printStackTrace();
}
}
}
上面使用JOptionPane弹出一个输入对话框让用户输入用户名,然后程序立刻将用户输入的用户名发送到服务器,服务器端会返回该用户名是否重复的提示,程序又读取服务器端提示,并根据服务器端提示判断是否需要继续让用户输入用户名
//程序判断用户输入的内容是否以/斜线开头,并包含冒号: ,如果满足,系统认为该用户想发送私聊消息
//就会将冒号之前的部分当成私聊,之后的部分当成公聊
public class ClientThread extends Thread {
//该客户端线程负责处理输入流
BufferedReader br=null;
//使用一个网络输入流来创建客户端线程
public ClientThread(BufferedReader br){
this.br=br;
}
@Override
public void run() {
try{
String line=null;
//不断地输入流中的读取数据,并将这些数据打印输出
while ((line=br.readLine())!=null){
System.out.println(line);
}
}catch (IOException ex){
ex.printStackTrace();
}//使用finally块来关闭线程对应的输入流
finally {
try{
if (br!=null){
br.close();
}
}catch (IOException ex){
ex.printStackTrace();
}
}
}
}
半关闭的Socket
在网络通信中则不能通过关闭输出流来表示输出已经结束,因为当关闭输出流时,该输出流对应的Socket也将会随之关闭,这样导致无法从该Socket的输入流中读取数据
这种情况,Socket提供了两个半关闭的方法,只关闭Socket的输入流或输出流,用以表示输出数据已经发送完成:
//该程序中服务器端先向客户端发送多条数据,数据发送完成后,该Socket对象调用关闭输出流方法
//表明数据发送结束--关闭输出流后,依然可以从Socket中读取数据
public class Server2 {
public static void main(String[] args) throws Exception {
ServerSocket ss=new ServerSocket(30000);
Socket socket=ss.accept();
PrintStream ps=new PrintStream(socket.getOutputStream());
ps.println("服务器的第一行数据");
ps.println("服务器的第二行数据");
//关闭Socket的输出流,表明输出数据已经结束
socket.shutdownOutput();
//下面语句将输出false,表明socket还未关闭
System.out.println(socket.isClosed());
Scanner scan=new Scanner(socket.getInputStream());
while (scan.hasNextLine()){
scan.close();
socket.close();
ss.close();
}
}
}
当调用Socketd的关闭输入流和输出流方法,该Socket无法再次打开输出流或输入流,因此这种做法通常不适宜保持通信状态的交互式应用,只适用于一站式的通信协议。
适用NIO实现非阻塞Socket通信
java提供了NIO API来开发高性能的网络服务器,适用NIO API则可以让服务器使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
java的NIO为非阻塞式Socket通信提供了如下几个特殊类:
Selector:他是SelectableChannel对象的多路复用器,所以希望才用非阻塞方式进行通信的Channel都应该注册到Selector对象,通过调用此类的open()静态方法创建Selector实例,该方法将使用系统默认的Selector来返回新的Selector
Selector可以同时监控多个SelectableChannel的IO状态,是非阻塞IO的核心。一个Selector实例有三个SelectionKey集合:
Selector还提供一系列的select()相关方法:
应用程序可调用SelectableChannel的register()方法将其注册到指定Selector上,当该Selector上的某些SelectableChannel上有需要处理的IO操作时,程序可以调用Selector实例select()方法返回获取它们的数量,并通过selectedKeys()方法返回它们对应的SelectionKey集合---通过该集合就可以获取有需要进行IO处理的SelecableChannel集。
SelecableChannel对象支持阻塞和非阻塞两种模式(所有的Channel默认都是阻塞模式),必须使用非阻塞模式才可以利用非阻塞IO操作。该对象提供两个方法设置和返回该Channel的模式状态
SelectableChannel还提供了如下几个方法来获取它的注册状态:
ServerSocketChannel可以接收客户端的连接请求,还需要调用Selector的select()方法来监听所有Channel上的IO操作
//服务器端
public class NServer {
//用于监测所有Channel状态的Selector
private Selector selector=null;
static final int PORT=30000;
//定义实现编码,解码的字符集对象
private Charset charset=Charset.forName("UTF-8");
public void init() throws IOException {
selector =Selector.open();
//通过open方法打开一个未绑定的ServerSocketChannel实例
ServerSocketChannel server=ServerSocketChannel.open();
InetSocketAddress isa=new InetSocketAddress("127.0.0.1",PORT);
//将该ServerSocketChannel绑定到指定ip地址
server.bind(isa);
//设置ServerSocket以非阻塞方式工作
server.configureBlocking(false);
//将server注册到指定的Selector对象
server.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select()>0){
//依次处理selector上的每个已选择的SelectionKey
for (SelectionKey sk : selector.selectedKeys()) {
//从selector上的已选择key集中删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果sk对应的Channel包含客户端的连接
if(sk.isAcceptable()){
//调用accept()方法接收连接,产生服务器端的SocketChannel
SocketChannel sc=server.accept();
//设置才用非阻塞模式
sc.configureBlocking(false);
//将该SocketChannel也注册到selector
sc.register(selector,SelectionKey.OP_ACCEPT);
//将sk对应Channel设置成准备接收其他请求
sk.interestOps(SelectionKey.OP_ACCEPT);
}
//如果sk对应的Channel有数据需要读取
if(sk.isReadable()){
//获取该SelectionKey对应的Channel,该Channel中有可读的数据
SocketChannel sc= (SocketChannel) sk.channel();
//定义准备执行读取数据的ByteBuffer
ByteBuffer buff=ByteBuffer.allocate(1024);
String content="";
//开始读取数据
try{
while(sc.read(buff)>0){
buff.flip();
content+=charset.decode(buff);
}
//打印从sk对应的Channel里读取到的数据
System.out.println("读取到的数据:"+content);
//将sk对应的Channel设置成准备下一次读取
sk.interestOps(SelectionKey.OP_ACCEPT);
}
//如果捕获到该sk对应的Channel出现了异常,即表明该Channel
//对应的Client出现了问题,所以从Selector中取消sk注册
catch (IOException ex){
//从Selector中删除指定的SelectionKey
sk.cancel();
if(sk.channel()!=null){
sk.channel().close();
}
}
//如果content的长度大于0,即聊天信息不为空
if(content.length()>0){
//遍历该selector里注册的所有SelectionKey
for (SelectionKey key : selector.selectedKeys()) {
//获取该key对应的Channel
Channel targetChannel=key.channel();
//如果该Channel是SocketChannel对象
if(targetChannel instanceof SocketChannel){
//将读到的内容写入Channel中
SocketChannel dest= (SocketChannel) targetChannel;
dest.write(charset.encode(content));
}
}
}
}
}
}
}
public static void main(String[] args) throws IOException {
new NServer().init();
}
}
上面程序启动时建立了一个可监听连接请求的ServerSocketChannel,并将Channel注册到指定的Selector,接着程序直接采用循环不断地监控Selector对象select()方法返回值,当该返回值大于0时,处理该Selector上的所有被选择的SelectionKey
开始处理指定的SelectionKey之后,立刻从该Selector上被选择的SelectionKey集合中删除该SelectionKey。
服务器端的Selector仅需要监听两种操作,连接和读数据,处理连接操作时,系统只需要连接完成后产生的SocketChannel注册到指定的Selector对象即可;处理读数据操作时,系统先从该Socket中读取数据,在将数据写入Selector上注册的所有Channel中
//客户端
//客户端需要两个线程,一个线程负责读取用户的键盘输入,并将输入的内容写入SocketChannel中;
//另一个线程则不断查询Selector对象的select()方法的返回值,如果大于0,那么程序要对相应的Channel执行IO操作
public class NClient {
//定义检测 SocketChannel的Selector对象
private Selector selector=null;
static final int PROT=20001;
//定义处理编码和解码的字符集
private Charset charset=Charset.forName("UTF-8");
//客户端SocketChannel
private SocketChannel sc=null;
public void init() throws IOException {
selector=Selector.open();
InetSocketAddress isa=new InetSocketAddress("127.0.0.1",PROT);
//调用open静态方法创建连接到指定主机的SocketChannel
sc=SocketChannel.open(isa);
//设置该sc以非阻塞方式工作
sc.configureBlocking(false);
//将SocketChannel对象注册到指定的Selector
sc.register(selector, SelectionKey.OP_READ);
//启动读取服务器端数据的线程
new ClientThread().start();
//创建键盘输入流
Scanner scan=new Scanner(System.in);
while (scan.hasNextLine()){
//读取键盘输入
String line=scan.nextLine();
//将键盘输入的内容输出到SocketChannel中
sc.write(charset.encode(line));
}
}
//定义读取服务器端数据的线程
private class ClientThread extends Thread{
@Override
public void run() {
try{
while (selector.select()>0){
//遍历每个有可用IO操作的 Channel对应的SelectionKey
for (SelectionKey sk : selector.selectedKeys()) {
//删除正在处理的SelectionKey
selector.selectedKeys().remove(sk);
//如果SelectionKey对应的Channel中有可读的数据
if(sk.isReadable()){
//使用NIOd刦Channel中的数据
SocketChannel sc= (SocketChannel) sk.channel();
ByteBuffer buff=ByteBuffer.allocate(1024);
String content="";
while (sc.read(buff)>0){
sc.read(buff);
buff.flip();
content+=charset.decode(buff);
}
//打印输出读取的内容
System.out.println("聊天信息:"+content);
//为下一次读取做准备
sk.interestOps(SelectionKey.OP_READ);
}
}
}
}catch (IOException e){
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
new NClient().init();
}
}
使用java7的AIO实现非阻塞通信
java7的NIO.2提供了异步Channel支持,它可以提供更高效的IO,这种基于异步Channel的IO机制也被称为异步IO(Asynchronous IO)
上图看出,NIO.2为AIO提供了两个接口和三个实现类,AsynchronousSocketChannel,AsynchronousServerSocketChannel是支持TCP通信的异步Channel。
AsynchronousServerSocketChannel:是一个负责监听Channel,与ServerSocketChannel相似,创建可用的AsynchronousSocketChannel需要如下两步:
- 调用它的open()静态方法创建一个未监听端口AsynchronousServerSocketChannel
- 调用AsynchronousServerSocketChannel的bind()方法指定该Channel在指定地址,指定端口监听。
AsynchronousServerSocketChannel的open()方法有两个版本:
使用AsynchronousServerSocketChannel只要三步:
- 调用open()静态方法创建AsynchronousServerSocketChannel
- 调用AsynchronousServerSocketChannel的bind()方法让他指定IP地址,端口监听
- 调用AsynchronousServerSocketChannel的accept()方法接受连接请求
public class SimpleAIOServer {
static final int PORT=30000;
public static void main(String[] args) throws Exception {
try(//创建AsynchronousServerSocketChannel对象
AsynchronousServerSocketChannel serverChannel=AsynchronousServerSocketChannel.open()){
//指定在指定地址,端口监听
serverChannel.bind(new InetSocketAddress(PORT));
while (true){
//采用循环接受来自客户端的连接
Future<AsynchronousSocketChannel> future=serverChannel.accept();
//获取连接完成后返回AsynchronousSocketChannel
AsynchronousSocketChannel socketChannel=future.get();
//执行输出
socketChannel.write(ByteBuffer.wrap("欢迎你来到AIO的世界".getBytes("UTF-8"))).get();
}
}
}
}
上面,当程序接受来自客户端的连接后,服务器端就产生了一个 客户端对应的AsynchronousSocketChannel写入数据的代码。
AsynchronousSocketChannel的用法分为三步
- 调用open()静态方法创建AsynchronousSocketChannel。调用open()方法时同样可指定一个AsynchronousChannelGroup作为分组管理器
- 调用AsynchronousSocketChannel的connect()方法连接到指定的IP地址,指定端口的服务器
- 调用AsynchronousSocketChannelde read(),write()方法进行读写
AsynchronousSocketChannel的connect(),read(),write()方法都有两个版本:一个是返回Future对象的get()方法返回时IO操作才真正完成;对于需要传入CompletionHandler参数的版本,通过CompletionHandler在IO操作完成时触发相应的方法
public class SimpleAIOClient {
static final int PORT=30000;
public static void main(String[] args) throws Exception {
//用于读取数据ByteBuffer
ByteBuffer buff=ByteBuffer.allocate(1024);
Charset utf=Charset.forName("UFT-8");
try(//创建AsynchronousSocketChannel对象
AsynchronousSocketChannel clientChannel=AsynchronousSocketChannel.open()){
//创建远程服务器
clientChannel.connect(new InetSocketAddress("127.0.0.1",PORT)).get();
buff.clear();
//从clientChannel中读取数据
clientChannel.read(buff).get();
buff.flip();
//将buff中的内容转换成字符串
String content=utf.decode(buff).toString();
System.out.println("服务器信息:"+content);
}
}
}
基于UDP协议的网络编程
UDP协议是一种不可靠的网络协议,它的通信实例的两端各建立在Socket,但两个Socket之间没有虚拟链路,这两个Socket只是发送,接收数据报的对象。java提供DatagramSocket对象作为基于UDP协议的Socket,使用DatagramPacket代表DatagramSocket发送,接收数据报。
UDP协议基础
UDP协议即用户数据报协议,主要用来支持那些需要在计算机之间传输数据的网络协议。
UDP协议是一种面向非连接的协议,面向非连接指的是在正式通信前不必与对方先建立连接,不管对方状态直接发送。至于对方是否接收到这些数据内容,UDP协议无法控制,因此说UDP协议是一种不可靠的协议。
UDP协议的主要作用是完成网络数据流和数据报之间的转换,在信息的发送端,UDP协议将网络数据流封装成数据报,然后将数据报发送出去;在信息的接收端,UDP协议将数据报转换成实际数据内容。
UDP协议和TCP协议简单对比:
TCP协议:可靠,传输大小无限制,但是需要连接建立时间,差错控制开销大
UDP协议:不可靠,差错控制开销比较小,传输大小限制在64KB一下,不需要建立连接
使用DatagramSocket发送,接收数据
java使用DatagramSocket代表UDP协议的Socket,DatagramSocket只是码头本身,不维护状态,不能产生IO流,它唯一的作用就是接收和发送数据报,java使用DatagramPacket来代表数据报,DatagramSocket接收和发送的数据都是通过DatagramPacket对象完成的。
DatagramSocket的构造器:
得到DatagramSocket实例后,就可以通过以下两个方法接收和发送数据:
使用DatagramSocket发送数据,DatagramSocket不知道将该数据报发送到哪里,而是由DatagramPacket自身决定数据报的目的地。
DatagramPacket的构造器:
在接收数据之前,应采用第一或第三个构造器生成一个DatagramPacket对象,给出接收数据字节数组以及长度。然后调用DatagramSocket的receive()方法等待数据报的到来,receive()将一直等待。
//创建一个接收数据DatagramPacket对象
DatagramPacket pack=new DatagramPacket(buf,256);
//接收数据
socket.receive(pack);
在发送数据之前,调用第二个或者第四个构造器撞见DatagramPacket对象,此时的字节数组里存放了想发的数据。除此之外,还要给出完整的目的地址,包括IP地址和端口号。发送数据是通过DatagramSocket的send()方法实现的,该方法根据目的地址寻径以传数据
//创建创建一个接收数据DatagramPacket对象
DatagramPacket packet=new DatagramPacket(buf,length,addrss,port);
//发送数据报
socket.send(packet);
当客户端或者服务器端接收一个DatagramPacket对象后,向该数据报发送者反馈信息,但是UDP协议是面向非连接的,所以接受者并不知道每个数据报由谁发送过来,但程序可以调用DatagramPacket如下三个方法获取发送者的IP地址和端口号:
//服务器端,使用循环1000次来读取DatagramPacket中的数据报,每当读取到内容之后便向该数据报的发送者发回一条信息
public class UdsServer {
public static final int PORT=30000;
//定义每个数据报的大小最大为4kb
public static final int DATA_LEN=4096;
//定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//以指定字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff,inBuff.length);
//定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket;
//定义一个字符串数组,服务器端发送该数组的元素
String[] books=new String[]{
"疯狂java", "轻量级javaEE","疯狂安卓","疯狂AJAX"
};
public void init() throws IOException {
try(//创建DatagramSocket对象
DatagramSocket socket=new DatagramSocket(PORT) ){
//才用循环接收数据
for (int i = 0; i < 1000; i++) {
//读取Socket中的数据,读到数据放入inPacket封装数组里
socket.receive(inPacket);
//判断inPacket.getData()和inBuff是否是同一个数组
System.out.println(inPacket.getData()==inBuff);
//将接收到的内容转换成字符串输出
System.out.println(new String(inBuff,0,inPacket.getLength()));
//从字符串数组中取出一个元素作为发送数据
byte[] sendData=books[i%4].getBytes();
//以指定的字节数组作为发送数据,以刚接收到的DatagramPacket的
//源SocketAddress作为目标SocketAddress创建DatagramPacket
outPacket=new DatagramPacket(sendData,sendData.length,inPacket.getSocketAddress());
//发送数据
socket.send(outPacket);
}
}
}
public static void main(String[] args) throws IOException {
new UdsServer().init();
}
}
//客户端采用循环不断地读取用户键盘输入,每当读取到用户输入的内容后将该内容封装DatagramPacket数据报
//在将该数据报发送出去;接着把DatagramSocket中的数据读入接收用DatagramPacket中。
public class UdpClient {
//定义发送数据报的目的地
public static final int DEST_PORT=30000;
public static final String DEST_IP="127.0.0.1";
//定义每个数据报的大小最大为4kb
private static final int DATA_LEN=4096;
//定义接收网络数据的字节数组
byte[] inBuff=new byte[DATA_LEN];
//以指定的字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket=new DatagramPacket(inBuff,inBuff.length);
//定义一个用于发送DatagramPacket对象
private DatagramPacket outPacket;
public void init() throws IOException {
try(//创建一个客户端DatagramSocket,使用随机接口
DatagramSocket socket=new DatagramSocket()){
//初始化发送用DatagramSocket,他包含一个长度为0的字节数组
outPacket=new DatagramPacket(new byte[0],0, InetAddress.getByName(DEST_IP),DEST_PORT);
//创建键盘输入流
Scanner scan=new Scanner(System.in);
//不断地读取键盘输入
while (scan.hasNextLine()){
//从键盘输入的一行字符串转换成字节数组
byte[] buff=scan.nextLine().getBytes();
//设置发送用的DatagramPacket中的字节数据
outPacket.setData(buff);
//发送数据报
socket.send(outPacket);
//读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
socket.receive(inPacket);
System.out.println(new String(inBuff,0,inPacket.getLength()));
}
}
}
public static void main(String[] args) throws IOException {
new UdpClient().init();
}
}
使用DatagramSocket进行网络通信,服务器端无须也无法保存每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立刻退出,但不管客户端是否退出,服务器端都无法知道客户端的状态
使用MulticastSocket实现多点广播
DatagramSocket只允许数据报发送给指定的目标地址,而MulticastSocket可以将数据报以广播的方式发送到数据量不等的多个客户端
若要使用多点广播,需要让一个数据报标有一组目标主机地址,当数据报发出后,整个组的所有主机都能接收到该数据报。IP地址多点广播实现了将单一信息发送到多个接收者的广播,其思想是设置一组特殊的网络地址作为多点广播地址,每个多点广播地址都被看作一个组,当客户端要发送,接收广播信息时,加入到该组即可。
IP协议为多点广播提供了这批特殊的IP地址,这些IP地址范围是224.0.0.0到239.255.255.255
通过Java实现多点广播时,MulticastSocket类是实现这一功能的关键,当MulticastSocket 把一个DatagramPacket发送到多点广播IP地址,该数据报将被自动广播到加入该地址的所有MulticastSocket。 MulticastSocket类既可以将数据报发送到多点广播地址,也可以接收其他主机的广播信息。
MulticastSocket有点像DatagramSocket,事实上MulticastSocket是DatagramSocket的一个 子类,也就是说MulticastSocket是特殊的DatagramSocket。若要发送一个数据报时,可使用随机端口创建 MulticastSocket,也可以在指定端口来创建MulticastSocket。
MulticastSocket提供了如下三个构造器:
public MulticastSocket():使用本机默认地址、随机端口来创建一个MulticastSocket对象。
public MulticastSocket(int portNumber):使用本机默认地址、指定端口来创建一个MulticastSocket对象。
public MulticastSocket(SocketAddress bindaddr):使用本机指定IP地址、指定端口来创建一个MulticastSocket对象。
创建一个MulticastSocket对象后,还需要将该MulticastSocket加入到指定的多点广播地址,MulticastSocket使用jionGroup()方法来加入指定组;使用leaveGroup()方法脱离一个组。
joinGroup(InetAddress multicastAddr):将该MulticastSocket加入指定的多点广播地址。
leaveGroup(InetAddress multicastAddr):让该MulticastSocket离开指定的多点广播地址。
在某些系统中,可能有多个网络接口。这可能会对多点广播带来问题,这时候程序需要在一个指定的网络接口上监听,通过调用setInterface可 选择MulticastSocket所使用的网络接口;也可以使用getInterface方法查询MulticastSocket监听的网络接口。
如果创建仅用于发送数据报的MulticastSocket对象,则使用默认地址、随机端口即可。但如果创建接收用的MulticastSocket对象,则该MulticastSocket对象必须具有指定端口,否则发送方无法确定发送数据报的目标端口。
MulticastSocket用于发送、接收数据报的方法与DatagramSocket的完全一样。但MulticastSocket比 DatagramSocket多一个setTimeToLive(int ttl)方法,该ttl参数设置数据报最多可以跨过多少个网络,当ttl为0时,指定数据报应停留在本地主机;当ttl的值为1时,指定数据报发送到本地 局域网;当ttl的值为32时,意味着只能发送到本站点的网络上;当ttl为64时,意味着数据报应保留在本地区;当ttl的值为128时,意味着数据报 应保留在本大洲;当ttl为255时,意味着数据报可发送到所有地方;默认情况下,该ttl的值为1。
从图17.7中可以看出,使用MulticastSocket进行多点广播时所有通信实体都是平等的,它们都将自己的数据报发送到多点广播IP地 址,并使用MulticastSocket接收其他人发送的广播数据报。下面程序使用MulticastSocket实现了一个基于广播的多人聊天室,程 序只需要一个MulticastSocket,两条线程,其中MulticastSocket既用于发送,也用于接收,其中一条线程分别负责接受用户键盘 输入,并向MulticastSocket发送数据,另一条线程则负责从MulticastSocket中读取数据。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Scanner;
//让该类实现Runnable接口,该类的实例可作为线程的target
public class MulticastSocketTest implements Runnable
{
//使用常量作为本程序的多点广播IP地址
private static final String BROADCAST_IP
= "230.0.0.1";
//使用常量作为本程序的多点广播目的的端口
public static final int BROADCAST_PORT = 30000;
//定义每个数据报的最大大小为4K
private static final int DATA_LEN = 4096;
//定义本程序的MulticastSocket实例
private MulticastSocket socket = null;
private InetAddress broadcastAddress = null;
private Scanner scan = null;
//定义接收网络数据的字节数组
byte[] inBuff = new byte[DATA_LEN];
//以指定字节数组创建准备接受数据的DatagramPacket对象
private DatagramPacket inPacket =
new DatagramPacket(inBuff , inBuff.length);
//定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket = null;
public void init()throws IOException
{
try
{
//创建用于发送、接收数据的MulticastSocket对象
//因为该MulticastSocket对象需要接收,所以有指定端口
socket = new MulticastSocket(BROADCAST_PORT);
broadcastAddress = InetAddress.getByName(BROADCAST_IP);
//将该socket加入指定的多点广播地址
socket.joinGroup(broadcastAddress);
//设置本MulticastSocket发送的数据报被回送到自身
socket.setLoopbackMode(false);
//初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0] , 0 ,
broadcastAddress , BROADCAST_PORT);
//启动以本实例的run()方法作为线程体的线程
new Thread(this).start();
//创建键盘输入流
scan = new Scanner(System.in);
//不断读取键盘输入
while(scan.hasNextLine())
{
//将键盘输入的一行字符串转换字节数组
byte[] buff = scan.nextLine().getBytes();
//设置发送用的DatagramPacket里的字节数据
outPacket.setData(buff);
//发送数据报
socket.send(outPacket);
}
}
finally
{
socket.close();
}
}
public void run()
{
try
{
while(true)
{
//读取Socket中的数据,读到的数据放在inPacket所封装的字节数组里。
socket.receive(inPacket);
//打印输出从socket中读取的内容
System.out.println("聊天信息:" + new String(inBuff , 0 ,
inPacket.getLength()));
}
}
//捕捉异常
catch (IOException ex)
{
ex.printStackTrace();
try
{
if (socket != null)
{
//让该Socket离开该多点IP广播地址
socket.leaveGroup(broadcastAddress);
//关闭该Socket对象
socket.close();
}
System.exit(1);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
public static void main(String[] args)
throws IOException
{
new MulticastSocketTest().init();
}
}
使用代理服务
java5开始,在java.net包下提供了Proxy和ProxySelector两个类
Proxy:代表一个代理服务器,可以打开URLConnection连接时指定Proxy,创建Socket连接时也可以指定Proxy
ProxySelector:代表一个代理选择器,它提供了对代理服务器更加灵活的控制,它可以对HTTP,HTTPS,FTp等进行分级设置,还可以设置不需要通过代理服务器的主机和地址,
直接使用Proxy创建连接
Proxy有一个构造器:Proxy(Proxy.Type type,SocketAddrss sa),用于创建表示代理服务器的Proxy对象,其中sa参数指定代理服务器的地址,type表示代理服务器的类型,该服务器类型有如下三种:
Proxy.Type.DIRECT:表示直接连接,不使用代理
Proxy.Type.HTTp:表示支持高级协议代理,如HTTP或FTP
Proxy.Type.SOCKS:表示SOCKS(V4或V5)代理
创建Proxy之后,程序就可以使用URLConnection打开连接,或者创建Socket连接时传入一个Proxy对象,作为本次连接所使用的代理服务器
URL包含一个URLConnection open(Proxy proxy)方法,该方法使用指定的代理服务器来打开连接;而Socket则提供了一个Socket(Proxy proxy)构造器,该构造器使用指定的代理服务器创建一个没有连接的Socket对象
//在URLConnection中使用代理服务器
public class ProxyTest {
//下面是代理服务器的地址和端口
//转成实际有效的代理服务器的地址和端口
final String PROXY_ADDR="129.82.12.188";
final int PROXY_PORT=3124;
//定义需要访问的网站地址
String urlStr="http://www.crazyit.org";
public void init() throws Exception {
URL url=new URL(urlStr);
//创建一个代理服务器对象
Proxy proxy=new Proxy(Proxy.Type.HTTP,new InetSocketAddress(PROXY_ADDR,PROXY_PORT));
//使用指定的代理服务器打开链接
URLConnection conn=url.openConnection(proxy);
//设置超时时长
conn.setConnectTimeout(3000);
try(//通过代理服务器读取数据的Scanner
Scanner scan=new Scanner(conn.getInputStream());
PrintStream ps=new PrintStream("index.htm")){
while(scan.hasNextLine()){
String line=scan.nextLine();
//在控制台输出页面资源内容
System.out.println(line);
//将网页资源内容输出到指定的输出流
ps.println(line);
}
}
}
public static void main(String[] args) throws Exception {
new ProxyTest().init();
}
}
程序中创建一个Proxy对象,使用该对象来打开URLConnection连接。接着使用URLConnection读取一份网络资源,此时的URLConnection并不是直接连接到urlStr的变量地址上,而是通过代理服务器去访问该网站
使用ProxySelector自动选择代理服务器
ProxySelector代表一个代理选择器,它本身是一个抽象类,程序无法创建他的实例,开发者可以考虑继承ProxySelector来实现自己的代理选择器。
实现方法很简单,程序只需要定一个继承ProxySelector的类,并让该类实现如下两个抽象方法:
实现了ProxySelector类之后,调用该类的setDefault(ProxySelector ps)静态方法来注册代理选择器即可
//让自定义ProxySelector来自动选择代理服务器
public class ProxySelectorTest {
//下面是代理服务器的地址和端口
//随便一个代理服务器的地址和端口
final String PROXY_ADDR="139.82.12.188";
final int PROXY_PORT=3124;
//定义需要访问的网站地址
String urlStr="http://www.crazyit.org";
public void init() throws Exception {
//注册默认的代理选择器
ProxySelector.setDefault(new ProxySelector() {
//根据业务需要返回特定的对应的代理服务器
@Override
public List<Proxy> select(URI uri) {
//本程序总是返回某个固定的代理服务器
List<Proxy> result=new ArrayList<>();
result.add(new Proxy(Proxy.Type.HTTP,new InetSocketAddress(PROXY_ADDR,PROXY_PORT)));
return null;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
System.out.println("无法连接到指定的代理服务器");
}
});
URL url=new URL(urlStr);
//没有指定代理服务器,直接打开连接
URLConnection conn=url.openConnection();
....
}
}
上面代码,才用匿名内部类实现了一个ProxySelector,这个ProxySelector的select()方法总是返回一个固定的代理服务器,也就是说,程序默认会使用该代理服务器。
java还提供了ProxySelector的一个实现类DefaultProxySelector,可通过ProxySelector.getDefult()方法来获取DefaultProxySelector实例
DefaultProxySelector继承ProxySelector,当然也实现了两个抽象方法,它的实现策略:
public class DefaultProxySelectorTest {
//定义需要访问的网站网址
static String urlStr="http://www.crazyit.org";
public static void main(String[] args) throws Exception {
//获取系统的默认属性
Properties props=System.getProperties();
//通过系统属性设置HTTP访问所用的代理服务器的主机地址和端口
props.setProperty("http://proxyHost","192.168.10.96");
props.setProperty("http.proxyPort","8080");
//通过系统属性设置HTTP访问无须使用代理服务器的主机
//可以使用通配符* ,多个地址用 | 隔开
props.setProperty("http.nonProxyHosts","localhost|192.168.10.*");
//通过系统属性设置HTTPS访问所用的代理服务器的主机地址,端口
props.setProperty("https.proxyHost","192.168.10.96");
props.setProperty("https.proxyPort","443");
/*DefaultProxySelector不支持https.nonProxyHosts属性
DefaultProxySelector直接按http.nonProxyHosts的设置规则处理
* */
props.setProperty("ftp.proxyHost","192.168.10.96");
props.setProperty("ftp.proxyPort","2121");
//通过系统属性设置FTP访问无须使用代理服务器的主机
props.setProperty("ftp.nonProxyHosts","localhost|192.18.10.*");
//通过系统属性设置SOCKS代理服务器的主机地址,端口,
props.setProperty("socks.ProxyHost","192.168.10.96");
props.setProperty("socks.ProxyPort","1080");
//获取系统默认的代理选择器
ProxySelector selector=ProxySelector.getDefault();
System.out.println("系统默认的代理选择器:"+selector);
//根据URI动态决定所使用的代理服务器
System.out.println("系统为ftp://www.crazyit.org选择的代理服务器:"+ProxySelector.getDefault().select(new URI("ftp://www.crazyit.org")));
URL url=new URL(urlStr);
//直接打开连接,默认的代理选择器会使用http.proxyHost,http.proxyPort系统属性
//设置代理服务器
//如果无法连接代理服务器,则默认的代理服务器会尝试直接连接
URLConnection conn=url.openConnection();
//设置超时时长
conn.setConnectTimeout(3000);
try(Scanner scan=new Scanner(conn.getInputStream(),"utf-8");){
//读取远程主机的内容
while (scan.hasNextLine()){
System.out.println(scan.hasNextLine());
}
}
}
}
上面程序返回了系统默认注册的ProxySelector,并返回DefaultProxySelector实例。程序中设置了HTTP访问的代理服务器属性,其中前两行设置了代理服务器地址和端口,第三行设置HTTP访问哪些主机时不需要使用代理服务器。
上面程序打开了一个URLCannection,系统会在打开该URLCannection时使用代理服务器。
由于192.168.10.96通常并不是有效的代理服务器,因此程序将会等待几分钟,无法连接到指定的代理服务器,默认的代理选择器connectFailed()方法被回调,该方法会尝试不使用代理服务器,直接连接远程资源