网络编程
- 关注的是底层数据的传输;
- 与网页编程不同,网页编程是关注与用户的数据交互。
概念:
-
网络:
将不同区域的计算机连接到一起;区域(局域网,城域网,互联网)
-
地址:
IP地址:
确定网络上一个绝对地址,位置;——》房子的地址 -
端口号:
区分计算机的软件——》房门 2个字节 0-65535 共65536个
1、在同一个协议下 端口号不能重复;不同协议下可以重复
2、1024以下的不要使用,给知名厂商预留的,如80——》 http;21——》ftp 尽量往大了指定 -
资源定位:
URL统一资源定位符;URI:统一资源。 -
数据的传输
1.协议:
TCP和UDP两种协议
1)TCP:面向连接,安全可靠;电话 类似于三次握手;效率低
2)UDP:非面向连接,效率高;短信2.传输数据:
1)先封装
2)后拆封
在Java中所涉及的类主要包括:=========》类
- IP(地址,端口):InetAddress、netSocketAddress
- URL
- TCP: ServerSocket 、Socket
- UDP: DatagramSocket 、 DatagramPacket
Java中相关类的学习
网络编程相关的类与接口位于java.net包中
一、地址与端口
InetAddress类,封装计算机的IP地址和域名解析DNS,但是没有端口
需掌握的有:
该类没有公开的构造器,所以构造对象应使用本身提供的静态方法。
1)静态方法获取对象:
InetAddress.getLocalHost();产生一个本机的InetAddress对象
InetAddress.getByName(“IP地址或者域名”);通过IP或域名产生一个InetAddress对象
2)调用对象的方法:
getHostAddress();返回对象的IP地址
getHostName();返回对象的域名,若无或者不能解析,则返回IP地址
InetSocketAddress类,封装端口,在InetAddress类基础上 + 端口
1)含有对外构造器,故用构造方法产生对象:
InetSocketAddress(String hostname, int port);主机名与端口号构造对象
InetSocketAddress(InetAddress addr, int port);IP地址与端口号构造对象
2)调用对象的方法:
getAddress();返回IP地址IntAddress;
getHostName();返回域名hostname
getPort();返回端口号
构造端口的例子:
1.InetSocketAddress address = new InetSocketAddress(“127.0.0.1”,9999);
等价于:
2. Address = new InetSocketAddress(InetAddress.getByName(“127.0.0.1”),9999);
因为第一种方法的源码中就存在着将字符转化成为IP地址InetAddress的代码;故这两种方法是等价的。
二、 资源定位
URL包含四部分:协议+域名+端口号+资源文件名(/之后开始算,是相对路径)
URL类:
表示指向互联网资源的指针
1)创建对象:
URL(String spec);绝对路径构建
URL(URL context, String spec);相对路径构建
2)方法:
getProtocol();返回协议
getHost();返回域名
getPort();返回端口号
getFile();返回资源
getPath();获取相对路径
getRef();返回锚点
getQuery();返回参数;在有锚点情况下返回null;无锚点则返回正确
3)资源流
openStream();返回一个输入流InputStream
构建URL与使用方法的一个例子:
1.绝对URL
url = new URL(“http://www.baidu.com:80/index.html#aa?uname=bjsxt”);
2.相对URL
url = new URL(“http://www.baidu.com:80/a/”);
url = URL(url, “b.txt”);
System.out.println(url.toString()); //http://www.baidu.com:80/a/b.txt
对第一种构建对象使用方法:
System.out.println(“协议:”+ url.getProtocol()); //协议:http
System.out.println(“域名:”+ url.getHost()); //域名:www.baidu.com
System.out.println(“端口:”+ url.getPort()); //端口:80
System.out.println(“资源:”+url.getFile());//资源:index.html#aa?uname=bjsxt
System.out.println(“相对路径:”+url.getPath()); //相对路径:index.html
System.out.println(“锚点:”+url.getRef()); // 锚点:aa?uname=bjsxt
System.out.println(“参数:”+url.getQuery()); //参数:有锚点返回null,若无锚点则返回uname=bjsxt
爬虫原理的一个例子:(爬虫第一步,先获取网页数据)
public static void main(String[] args){
URL url = new URL(“http://www.baidu.com”); //主页,默认资源
//获取资源,网络流
/* //直接这样读取的主页数据会存在乱码,主要是由于编码与解码的字符集不统一造成的,所以可以用转换流
InputStream is = url.openStream();
byte[] flush = new byte[1024];
int len = 0;
while((len=is.read(flush))!= -1){
System.out.println(new String(flush,0,len));
}
is.close();
*/
//使用转换流
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(),”utf-8”));
//也可以再定义写入流将读取到的内容直接写到文件中
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(baidu.html),”utf-8”));
String msg = null;
while((msg=br.readLine())!=null){
System.out.println(msg); //直接打印出来内容
bw.append(msg);
bw.newLine();
}
bw.flush();
bw.close();
br.close();
}
三、UDP传输数据
UDP是以数据为中心的,非面向连接的,不安全,但效率高
根据数据传输的对象可分为客户端与服务器端;
在java种主要涉及到***DatagramSocket类***以及***DatagramPacket类***,其传输步骤分别为:
1.客户端
1)创建客户端对象,DatagramSocket类 + 指定端口
2)准备数据 字节数组
3)打包 DatagramPacket类 + 服务器地址及端口
4)发送数据
5)释放资源
2.服务器端
1)创建服务器对象,DatagramSocket类 + 指定端口
2)准备接收容器 字节数组 封装 DatagramPacket类
3)包 接收数据
4)分析数据
5)释放资源
服务器端与客户端使用的类是相同的,不过使用的方法有些不同:
DatagramSocket类 发送和接收数据报数据包的套接字。
1)创建对象 均使用DatagramSocket(int port)
2)方法
send(DatagramPacket p) 通过数据包p发送数据
receive(DatagramPacket p)从数据包p接收数据
close()关闭套接字,释放资源
DatagramPacket类 该类表示数据报包
数据封装与拆封操作均是DatagramPacket类,
客户端打包数据:
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address)
//此处的SocketAddress是包含了接收端主机IP和端口的InetSocketAddress
服务端准备包接收数据:
DatagramPacket(byte[] buf, int length)
//主需要准备一个byte接收数组即可,不需要知道发送方的信息。
一般先写服务端,再写客户端:
服务端保存为:MyServer.java
public static void main(String[] args){
//1、创建服务器端 + 端口
DatagramSocket server = new DatagramSocket(8888);
//2、准备接收容器DatagramPacket(byte[] buf, int length)
byte[] container = new byte[1024];
//3、封装成包 DatagramPacket(byte[] buf, int length)
DatagramPacket packet = new DatagramPacket(container, container.length);
//4、接收数据
server.receive(packet);
//5、分析数据
byte[] data = packet.getData();
int len = packet.getLength();
System.out.println(new String(data,0,len));
//6、释放资源
server.close();
}
客户端保存为:MyClient.java
public static void main(String[] args){
//1、创建客户端 + 端口
DatagramSocket client = new DatagramSocket(6666);
//2、准备数据
String msg = “UDP编程”;
byte[] data = msg.getBytes();
//3、封装成包(指定发送地点及端口)DatagramPacket(byte[] buf, int length, SocketAddress address)
DatagramPacket packet = new DatagramPacket(data,data.length,new InetSocketAddress(“localHost”,8888));
//4、发送数据
client.send(packet);
//5、释放资源
client.close();
}
上述实现的功能是将字符数组从客户端发送到服务端;而如何将其他类型的数据发送呢?
由于数据的传输只能一字节数组的形式,所以数据在打包和拆封时均需要进行相对应的转换;上述其实已经存在String——》byte[]的转换:byte[] data = msg.getBytes();以及相对应的byte[]——》String转换:new String(data,0,len)。
所以对于其他类型的数据,我们发送前也先要将其转换为byte[]形式,接收后也需要将其转化为原来的形式。
客户端的转换
转换方式double——》byte[] 字节数组 数据源 + Data输出流
public static byte[] convert(double num) throws IOException{
byte[] data = null;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeDouble(num);
dos.flush();
//获取数据
data = bos.toByteArray();
dos.close();
return data;
}
类型转换结束后再将客户端的步骤2修改为:
double num = 89.12;
byte[] data = convert(num);
服务端的转换
转换方式 字节数组——》double 字节数组+ Data输入流
public static double convert(byte[] data) throws IOException {
DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data));
//获取数据
double num = dis.readDouble();
dis.close();
return num;
}
完成转换后再将服务端的分析数据步骤5修改为:
double data = convert(packet.getData());
四、TCP数据传输
TCP是面向连接的,安全,可靠,效率低;这个连接对服务端与客户端是同一个Socket。
1.面向连接Request—Response。
2.Socket编程
服务端ServerSocket类
1)构建对象:ServerSocket(int port)需指明端口
2)接收客户端连接:accept()阻塞式接收
3)发送数据;一个连接Socket相当于双向流,所以可以由Socket中getOutputStream()方法获得输出流OutputStream;
客户端Socket类
1)创建对象:Socket(String host, int port)指明服务器IP及端口;客户端本身不需要指明端口,由客户端自动分配。
客户端建立成功即表示建立了连接,一个接收accept()只能连接一次
2)接收数据:先使用Socket中getInputStream()方法获得输入流InputStream由于服务端与客户端的连接是同一个,所以两端对流进行包装时需要用同样的种类,即若服务端用BufferedWriter,则客户端要用BufferedReader;同样若服务端用DataOutputStream,则客户端也要用DataInputStream
如何在一个服务端创建多个客户端的连接?
若直接在服务端的接受连接accept()处建立循环,则实现的功能实际上是服务器先与一个客户端 建立连接,然后与之通信完毕后再与其他客户端建立连接。这样每个客户端之间并不是独立的,若一个客户端的连接出现阻塞式服务,则所有客户端均需等待。
实际上多个客户端连接需要的是每个客户端都能与服务器独立的发送,接收数据;并且客户端之间也是独立的;所以需要服务器为每一个客户端的连接创建一个线程,且需要为客户端的发送与接收分别创建一个线程。
简易聊天室原理:
先写服务器:Server01.java
public static void main(String[] args){
//创建服务器
ServerSocket server = new ServerSocket(9999);
//接收服务端连接
Socket client = server.accept();
//写出数据
//输入流
DataInputStream dis = new DataInputStream(client.getInputStream());
String msg = dis.readUTF();
//输出流
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
dos.writeUTF(“服务器——>” + msg);
dos.flush();
}
再写客户端:Client01.java
public static void main(String[] args){
//创建客户端
Socket client = new Socket(“localhost”,9999);
//创建控制台输入流
BufferedReader console = new BufferedReader(new InputStreamReader(System.in));
String info = console.readLine();
//输出流
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
dos.writeUTF(info);
//输入流
DataInputStream dis = new DataInputStream(client.getInputStream());
String msg = dis.readUTF();
System.out.println(msg);
}
在Client01.java中,客户端只能单次读入,为了能多次读入信息并发送,需要进行循环,在循环中不需要重复建立流,所以可以将流单独出来,将其处理进行循环即可。
针对上述聊天室,客户端只能先读取控制台数据,然后发送数据,最后接收数据;服务端只能先接收数据,然后转发数据;即数据的收发并不是独立的,所以需要建立线程单独控制,如下:
先建立发送线程Send.java
public class Send implements Runnable{
//控制台输入流
private BufferedReader console;
//输出流
private DataOutputStream dos;
//线程标识
private boolean isRunning = true;
//构造器
public Send(){
console = new BufferedReader(new InputStreamReader(System.in));
}
public Send(Socket client){
this.Send();
try{
dos = new DataOutputStream(client.getOutputStream());
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dos, console);
}
}
//1.从控制台接收数据
public String getMsgFromConsole(){
try{
return console.readLine();
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dos, console);
}
return “”;
}
//将接收的数据发送出去
public void send(){
String msg = getMsgFromConsole();
try{
if(msg != null && !msg.equals(“”)){
dos.writeUTF(msg);
dos.flush();
}
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dos, console);
}
}
public void run(){
//线程主体
while(isRunning){
send();
}
}
}
然后在客户端处连接后直接建立新的线程即可:
new thread(new Send(client)).start(); //一条路径
类似的建立接收线程Receive.java
public class Receive implements Runnable{
//输入流
private DataInputStream dis;
//线程标识
private boolean isRunning = true;
//构造器
public Receive(){
}
public Receive(Socket client){
try{
dis = new DataInputStream(client.getInputStream());
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dis);
}
}
//接收数据
public String receive(){
String msg = “”;
try{
msg = dis.readUTF();
}catch(IOException e ){
isRunning = false;
CloseUtil.closeAll(dis);
}
return msg;
}
public void run(){
//线程主体
while(isRunning){
System.out.println(receive());
}
}
}
将关闭流的方法进行封装:CloseUtil.java
public class CloseUtil{
public static void closeAll(Closeable… io){
for(Closeable temp:io){
try{
if(temp !=null){
temp.close();
}
}catch(Exception e){
}
}
}
}
线程封装完毕后再将客户端进行修改Client02.java
public static void main(String[] args){
//创建客户端
Socket client = new Socket(“localhost”,9999);
//创建输入线程
new Thread(new Send(client)).start();
//创建输入线程
new Thread(new Receive(client)).start();
}
这样客户端在连接之后便可以任意发送与接收数据了,但是服务端目前还是只接受与转发数据一次,所以需要将服务端加一个循环。Server02.java
public static void main(String[] args){
//创建服务器
ServerSocket server = new ServerSocket(9999);
//接收服务端连接
Socket client = server.accept();
//写出数据
//输入流
DataInputStream dis = new DataInputStream(client.getInputStream());
//输出流
DataOutputStream dos = new DataOutputStream(client.getOutputStream());
while(true){
String msg = dis.readUTF();
dos.writeUTF(“服务器——>” + msg);
dos.flush();
}
}
这样便可以实现一个客户端与服务端任意的收发数据了,但是还不能实现多个客户端的连接。主要是因为服务端并没有为每个客户端的连接创建单独的线程,由于这个服务线程只有服务端使用,所以可以考虑在服务端建立内部类,创建如下:
服务端Server03.java
public class Server03{
//为每个客户端创建一个容器
private List< MyChannel > all = new ArrayList< MyChannel >();
public static void main(String[] args){
new Server03. channel();
}
//接收客户端线程服务
public void channel(){
//创建服务器
ServerSocket server = new ServerSocket(9999);
//接收服务端连接
while(true){
Socket client = server.accept();
MyChannel channel = new MyChannel(client);
all.add(channel); //将一条道路加入容器
new Thread(channel).start();
}
}
//内部类,处理客户端的线程
private class MyChannel implements Runnable(){
private DataInputStream dis;
private DataOutputStream dos;
private boolean isRunning = true;
//构造器
public MyChannel(){
}
public MyChannel(Socket client){
try{
dis = new DataInputStream(client.getInputStream());
dos = new DataOutputStream(client.getOutputStream());
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dis,dos);
all.remove(this): //移除自身
}
}
//接收数据
pivate String receive(){
String msg = “”;
try{
msg = dis.readUTF();
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dis);
all.remove(this): //移除自身
}
return msg;
}
//发送数据
private void send(String msg){
try{
if(msg!=null && !msg.equals(“”)){
dos.writeUTF(msg);
dos.flush();
}
}catch(IOException e){
isRunning = false;
CloseUtil.closeAll(dos);
all.remove(this): //移除自身
}
}
//发送给其他客户端
private void sendOthers(){
String msg = receive();
for(MyChannel other : all){
if(other == this){
continue;
}
other.send(msg)
}
}
public void run(){
//线程主体
while(isRunning){
sendOthers();
}
}
}
}
这样便实现了客户端与服务器端的独立通信。