网络编程
网络模型:
OSI(Open System Interconnection开放系统互连)参考模型
OSI模型把网络通信的工作分为7层。1至4层被认为是低层,这些层与数据移动密切相关。5至7层是高层,包含应用程序级的数据。
第1层物理层:原始比特流的传输。集线器、网线
第2层数据链路层:在此层将数据分帧,并处理流控制。本层指定拓扑结构并提供硬件寻址。网卡,网桥,交换机
第3层网络层:本层通过寻址来建立两个节点之间的连接,它包括通过互连网络来路由和中继数据。路由器,防火墙
第4层传输层:常规数据递送-面向连接或无连接。包括全双工或半双工、流控制和错误恢复服务。计算机的进程和端口
第5层会话层:在两个节点之间建立端连接。此服务包括建立连接是以全双工还是以半双工的方式进行设置,尽管可以在层4中处理双工方式。建立会话,SESSION认证、断点续传
第6层表示层:格式化数据,以便为应用程序提供通用接口。这可以包括加密服务。编码方式,图像编解码、URL字段传输编码
第7层应用层:直接对应用程序提供服务,应用程序可以变化,但要包括电子消息传输。应用程序,如FTP,SMTP,HTTP
TCP/IP参考模型
这个模型只有四层。主机至网络层,网际层,传输层,应用层。
网络通讯要素:IP地址 端口号 传输协议
IP地址:InetAddress
网络中设备的标识,长度为4个字节,也就是32个0和1的数字编码,为了方便使用,写成十进制。比如192.168.1.100,没个数不超过255,因为一个字节,8个0和1的数最多表示255。
本地回环地址:127.0.0.1 它代表设备的本地虚拟接口,只用来访问本机,不能访问其他机器。一般都会用来检查本地网络协议、基本数据接口等是否正常的。
IP地址不易记忆,就有了主机名:localhost,就是计算机的名字,这个名字可以随时更改。
端口号:用于标识进程的逻辑地址,不同进程的标识。
有效端口:0-65535,其中0-1024系统使用或保留端口。
传输协议:通讯的规则
常见协议:
TCP 传输控制协议
建立连接,形成数据传输通道
在连接中进行大量数据传输
通过三次握手完成链接,是可靠协议
必须建立连接,效率稍低
UDP 数据报文协议
将数据及源和目的封装成数据包中,不需要建立连接
每个数据报的大小限制在64K内
因无连接,是不可靠协议
不需要建立连接,速度快
package day25;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IPDemo {
public static void main(String[] args) throws UnknownHostException {
//获取本地主机IP对象
InetAddress ip=InetAddress.getLocalHost();
System.out.println(ip.getHostAddress());
System.out.println(ip.getHostName());
//获取其他主机的IP地址对象
ip=InetAddress.getByName("www.baidu.com");
System.out.println(ip.getHostName());
System.out.println(ip.getHostAddress());
}
}
解析地址的过程:
平常上网IP地址不便于记忆,比如新浪的IP是10.0.0.1 上新浪网总不能输入这个吧,太难记了。
我们输的是www.sina.com 。 那网址就要和IP对应啊,所以输了这个不是直接就去新浪了,先找DNS(域名解析器),它专门记录域名和IP的对应关系。它会返回给你一个IP,然后你就拿着这个IP才去了新浪。
域名解析时先走本机的hosts域名解析文件,解析失败后才走DNS。
所以平常有广告网页时,把他的域名复制,写到hosts文件里,对应的IP写成本机127.0.0.1 ,域名解析先走本地文件,这样就可以屏蔽广告了。
Socket(可以理解为插头,港口)
Socket就是为网络服务提供的一种机制。通信的两端都有Socket。
网络通信就是Socket间的通信,数据在两个Socket间通过IO传输。
UDP传输
DatagramSocket 用来发送和接收数据包的套接字。
DatagramPacket 此类表示数据报包。数据报包用来实现无连接包投递服务。
UDP传输的发送端:
package day25;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPSendDemo {
/**
* UDP传输的发送端、
* 1.建立UDP的socket服务。
* 2.将要发送的数据封装到数据包中
* 3.通过UDP的socket服务将数据包发送出去
* 4.关闭socket服务
*
* 注意:在我们写的这个例子中得先打开接收端,而不是发生端。
*
*/
public static void main(String[] args) throws IOException {
System.out.println("发送端启动。。。");
//1.建立UDP的socket服务。使用DatagramSocket对象
DatagramSocket ds=new DatagramSocket();
//2.将要发送的数据封装到数据包中。使用DatagramPacket将数据封装到该对象包中。
String str="UDP传输演示:哥们来了!";
byte[] buf=str.getBytes();
DatagramPacket dp=new DatagramPacket(buf, buf.length, InetAddress.getByName("192.168.31.74"), 10000);
//3.通过UDP的socket服务将数据包发送出去。使用send方法
ds.send(dp);
//4.关闭socket服务
ds.close();
}
}
UDP传输的接收端:
package day25;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPReceiveDemo {
/**
* UDP传输的接收端
* 1.建立UDP的socket服务。 因为是要接收数据,所以必须明确端口号,和发送端口号一致才行。不指定端口号就是随机的,就收不到。
* 2.创建数据包,用于存储接收到的数据,方便用数据包对象的方法解析数据。
* 3.使用socket服务的receive方法将接受的数据存储到数据包中。
* 4.通过数据包对象的方法解析数据包中的数据。
* 5.关闭资源。
*
* 注意:在我们写的这个例子中得先打开接收端,而不是发生端。
*/
public static void main(String[] args) throws IOException {
System.out.println("接收端启动。。。");
//1.建立UDP的socket服务。使用DatagramSocket对象
DatagramSocket ds=new DatagramSocket(10000); //端口号必须和发送的端口号一致才行。
//2.创建数据包,用于存储接收到的数据,方便用数据包对象的方法解析数据。
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
//3.使用socket服务的receive方法将接受的数据存储到数据包中。
ds.receive(dp); //阻塞式的方法。
//4.通过数据包对象的方法解析数据包中的数据。比如IP地址,端口,数据内容等
String ip=dp.getAddress().getHostAddress();
int port=dp.getPort();
String text=new String(dp.getData(),0,dp.getLength());
System.out.println(ip+":"+port+":"+text);
//5.关闭资源
ds.close();
}
}
练习:实现一个简易的聊天软件
package day25;
import java.net.DatagramSocket;
import java.net.SocketException;
public class ChatDemo {
/**
* 因为有可能发送的同时要接受,所以要用到多线程
* 要实现群聊功能,只需要把chatsend中发送的IP地址最后一个字节改为255即可。
* 改成255就会发送给该网段的所有人。
* @param args
* @throws SocketException
*/
public static void main(String[] args) throws SocketException {
DatagramSocket send=new DatagramSocket();
DatagramSocket receive=new DatagramSocket(8000);
ChatSend s=new ChatSend(send);
ChatReceive r=new ChatReceive(receive);
new Thread(s).start();
new Thread(r).start();
}
}
发送模块
package day25;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class ChatSend implements Runnable {
private DatagramSocket ds;
public ChatSend(DatagramSocket ds){
this.ds=ds;
}
@Override
public void run() {
try {
BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in));
String line=null;
while((line=bufr.readLine())!=null){
byte[] buf=line.getBytes();
DatagramPacket dp=new DatagramPacket(buf, buf.length, InetAddress.getByName("192.168.31.74"), 10000);
ds.send(dp);
if("over".equals(line)){
break;
}
ds.close();
}
} catch (Exception e) {
}
}
}
接收模块
package day25;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class ChatReceive implements Runnable {
private DatagramSocket ds;
public ChatReceive(DatagramSocket ds){
this.ds=ds;
}
@Override
public void run() {
try{
while(true){
byte[] buf=new byte[1024];
DatagramPacket dp=new DatagramPacket(buf,buf.length);
ds.receive(dp);
String text=new String(dp.getData(),0,dp.getLength());
if(text.equals("over")){
System.out.println("对方已退出");
}
System.out.println(text);
}
}catch(Exception e){}
}
}
TCP传输
面向连接,要经过3次握手连接才能形成数据通道。
客户端:
package day25;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class TCPClientDemo {
/**
* 需求:客户端发送数据到服务端。
* tcp要经过3次握手连接,才能形成数据传输通道。
*
* 注意:tcp是面向连接的,运行时必须先开服务端。
* @throws IOException
* @throws UnknownHostException
*/
public static void main(String[] args) throws UnknownHostException, IOException {
/*
* Tcp传输客户端建立的过程:
* 1.创建tcp客户端socket服务,使用的是cosket对象。建议该对象一创建就明确目的地,即要连接的主机。
* 2.如果建立连接成功,就会形成数据传输通道Socket流(网络IO流)。
* Socket流是底层建立好的,既能输入又能输出。想要输出或者输入对象,可以找socket获取。
* 可以通过getInputStream()和getOutputStream来获取两个字节流。
* 3.使用输出流,将数据写出。
* 4.关闭资源。
*/
//创建客户端socket服务
Socket socket=new Socket("192.168.37.74",6666); //创建对象时最好就明确发送地址的IP和端口
//获取socket流中的输出流
OutputStream out=socket.getOutputStream();
//使用输出流将指定的数据写出去
out.write("tcp演示:哥们来了!".getBytes());
//关闭资源
socket.close();
}
}
服务端:
package day25;
import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServerDemo {
/**
* 需求:服务端接收客户端发送过来的数据,并打印在控制台上。
* 注意:tcp是面向连接的,运行时必须先开服务端。
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
/*
* Tcp传输服务端建立的过程:
* 1.创建服务端socket服务。通过ServerSocket对象。
* 2.服务端必须对外提供一个端口,否则客户端无法连接。
* 3.获取连接过来的客户端对象。
* 4.通过客户端对象获取socket流读取客户端发来的数据并打印。
* 5.关闭资源。这个不仅要关服务端,还得关客户端。
*/
//创建服务端对象
ServerSocket ss=new ServerSocket(95232);
//获取连接过来的客户端对象
Socket s=ss.accept(); //阻塞式方法
String ip=s.getInetAddress().getHostAddress();
//通过socket对象获取输入流,读取客户端发来的数据
InputStream in=s.getInputStream();
byte[] buf=new byte[1024];
int len=in.read(buf);
String text=new String(buf,0,len);
System.out.println("Server"+ip+"..."+text);
//关闭资源
s.close();
ss.close();
}
}
有时候会出现服务端和客户端都在等的情况,原因是用了阻塞式方法,具体可能有以下几个问题:
1.客户端根本没写进去。只写到了socket输出流里,没写到socket.getOutputStream()里面。给socket流加个刷新就行。
2.缺少结束标记。服务端读到换行才会结束读取,返回数据,所以加上"\r\n"。注意服务端和客户端都要加,因为两边都要读取数据。
解决上面两个问题的简便方法就是socket输出流直接用PrintWriter。
创建PrintWriter对象时直接加上true就能自动刷新了。
输出时,直接用println方法,直接就能换行。
练习:
需求:客户端输入字母数据,发送给服务端,服务端收到后显示到控制台。
并将该数据转成大写返回给客户端,直到客户端输入over,转换结束。
说白了,就是创建一个英文大写转换服务器。
客户端:
package day25;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class TransClient {
public static void main(String[] args) throws UnknownHostException, IOException{
/*
* 思路:
* 1.客户端要先有socket断点
* 2.客户端的数据源:键盘
* 3.客户端的目的:socket
* 4.接收服务端的数据, 源:socket
* 5.将数据打印出来 目的:控制台
* 6.在这些流中操作的数据都是文本数据。可以用字符流操作,方便。
*
* 步骤:
* 1.创建socket客户端对象。
* 2.获取键盘录入
* 3.将录入的信息发送给socket输出流
*/
//创建socket客户端对象
Socket s=new Socket("192.168.1.113",10005);
//获取键盘录入
BufferedReader bufr=new BufferedReader(new InputStreamReader(System.in));
//socket输出流
PrintWriter out=new PrintWriter(s.getOutputStream(),true); //true自动刷新
//也可以这样写 BufferedWriter bufw=new BufferedWriter(new OutputStreamWriter(s.getOutputStream(),true);
//socket输入流,读取服务端返回的数据
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
String line=null;
while((line=bufr.readLine())!=null){
if("over".equals(line))
break;
out.println(line);
//读取服务端返回的数据
String upperStr=bufIn.readLine();
System.out.println(upperStr);
}
s.close();
}
}
服务端:
package day25;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class TransServer {
public static void main(String[] args) throws IOException{
/*
* 思路:
* 1.serversocket服务。
* 2.获取socket对象。
* 3.读取客户端发过来的数据 源:socket
* 4.目的:显示在控制台上
* 5.将数据转成大写发回给客户端。
*/
ServerSocket ss=new ServerSocket(10005);
//获取socket对象。
Socket s=ss.accept();
//获取连接进来计算机的IP
String ip=s.getInetAddress().getHostAddress();
System.out.println(ip+"...connected");
//获取socket读取流,并装饰
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
//获取socket输出流,并装饰
PrintWriter out=new PrintWriter(s.getOutputStream(),true); //true是自动刷新的
String line=null;
while((line=bufIn.readLine())!=null){
System.out.println(line);
out.println(line.toUpperCase());
}
s.close();
ss.close();
}
}
需求:做一个上传文件的程序。
客户端:
package day25;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class UploadCilent {
/**
* 注意问题:1.默认缓冲区大小是8kb,装满后悔自动刷新一次,
* 所以如果文件超过8kb而且bufw不进行刷新操作,就会发现上传的数据不完整。
* 2.服务端的while循环没有结束标记。所以运行时会发现服务端和客户端都在等的问题。
* 只要在客户端发完数据后再发一个标记,服务端读到就可以结束了。
* 那样还是麻烦。java提供了一个更简单方便的结束方法,socket流的shutdown方法。
* @param args
* @throws UnknownHostException
* @throws IOException
*/
public static void main(String[] args) throws UnknownHostException, IOException {
Socket s=new Socket("192.168.1.113",10007);
BufferedReader bufr=new BufferedReader(new FileReader("client.txt"));
PrintWriter out=new PrintWriter(s.getOutputStream(),true);
String line=null;
while((line=bufr.readLine())!=null){
out.println(line);
}
s.shutdownOutput(); //java提供的更为简单方便的结束标记方法。
//out.print("over"); //给服务端一个结束的标记,要不结束不了。别用over,万一文本里也有over就完了。一般都用时间的毫秒值。
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
String str=bufIn.readLine();
System.out.println(str);
bufr.close();
s.close();
}
}
服务端:
package day25;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class UploadServer {
/**
* 注意问题:1.默认缓冲区大小是8kb,装满后悔自动刷新一次,
* 所以如果文件超过8kb而且bufw不进行刷新操作,就会发现上传的数据不完整。
* 2.服务端的while循环没有结束标记。所以运行时会发现服务端和客户端都在等的问题。
* 只要在客户端发完数据后再发一个标记,服务端读到标记就可以结束了。
* 那样还是麻烦。java提供了一个更简单方便的结束方法,socket流的shutdown方法。
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
ServerSocket ss=new ServerSocket(10007);
Socket s=ss.accept();
System.out.println(s.getInetAddress().getHostAddress()+".....connected");
BufferedReader bufIn=new BufferedReader(new InputStreamReader(s.getInputStream()));
BufferedWriter bufw=new BufferedWriter(new FileWriter("server.txt"));
String line=null;
while((line=bufIn.readLine())!=null){
bufw.write(line);
bufw.newLine(); //别忘了换行,要不写的数据贼乱。
bufw.flush(); //别忘了刷新,要不出问题,具体看上面的文档注释。
}
PrintWriter out=new PrintWriter(s.getOutputStream(),true);
out.println("上传成功!");
bufw.close();
s.close();
ss.close();
}
}
网络编程:
最常见的客户端: 浏览器 IE
最常见的服务端: 服务器 Tomcat
为了了解其原理:
1.自定义服务端使用已有的客户端IE,了解一下客户端给服务端发了什么请求?
自己写一个服务端,里面把客户端发过来的东西全打印出来,然后浏览器地址栏输入http://ip:服务器的端口/。这样就可以知道客户端给服务端到底发了什么请求。
发送的请求是:Get/HTTP/1.1 //第一行是请求行:请求方式(get和post)/请求的资源路径 HTTP协议版本
//下面这些都是请求消息头。 属性名:属性值
Accept:*/* //支持接收的文件格式,一般都很长,这里就没写
Accept-Language:zh-cn,zu-;q=0.5 //支持的语言
Accept-Encoding:qzip,deflate //支持的压缩方式
User-Agent:Mozilla/4.0(compatible;MSIE 6.0;Windows NT 5.1;SV1;InfoPath.2) //用户的一些信息
Host:192.168.1.100:9090 //要访问的主机
Connection:Keep-Alive
//空行
//下面有需要的话还有请求体。请求头和请求体中间有空行,方便服务器解析。
2.了解一下服务端给客户端发了什么?
和上面一个道理。自己写个客户端,把服务端发回来的信息全部打出来就行。
发回的信息是:HTTP/1.1 200 OK //应答行,HTTP协议版本 应答状态码(200表示成功,404表示服务端没有这个信息) 应答状态描述信息
//下面都是应答信息头。 属性名:属性值
Server:Apache-Coyote/1.1
ETag:W/"199-1323480176984"
Last-Modified:Sat,10 Dec 2011 01:22:56 GMT //上次访问缓存的时间,如果和服务器端一致,服务器就不返回信息,直接访问缓存好的,更快
Content-Type:text/html //文本类型
Content-length:199 //文本长度
Date:Fri,11 May 2012 07:51:39 GMT
Connection:close
//空行
//下面就是应答消息体。解析后就是网页显示的内容。
URL是统一资源定位符,也就是我们通常说的网址。
URI是同一资源标识符。URL是URI的子类。所以URL都是URI。
package day25;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
public class URLDemo {
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
String str_url="http://192.168.1.113:8080/myweb/1.html?name=lisi";
//问号后面的是参数信息,原来是欢迎光临,加上这个就可以变成欢迎lisi光临
URL url=new URL(str_url);
System.out.println("getProtocol:"+url.getProtocol());
System.out.println("getHost:"+url.getHost());
System.out.println("getPort:"+url.getPort());
System.out.println("getFile:"+url.getFile());
System.out.println("getPath:"+url.getPath());
System.out.println("getQuery:"+url.getQuery());
//本来自己写的客户端不能解析,输出总是把应答信息头也输出了,这是没有必要的
//而url的openStream()就可以解决,输出时就不会输出应答信息头。
//这个例子中我写的url的IP以及文件是视频中老师的,我没装tomcat,所以才显示不出来。
InputStream in=url.openStream();
/*
* url.openStream()内部实现的原理说白了就是openConnection()+getInputStream();
* URLConnection con=url.connection();
* 上面这句就是获取url连接器对象并封装成对象。java中内置的可以解析具体协议的对象+socket。
* InputStream in=con.getInputStream();
*/
byte[] buf=new byte[1024];
int len=in.read(buf);
String text=new String(buf,0,len);
System.out.println(text);
//url不用close
in.close();
}
}
网络结构:
1.C/S client/server
优点:客户端在本地可以分担一部分运算。
缺点:该结构的软件,客户端和服务端都需要编写。
开发成本较高,维护较为麻烦。
2.B/S browser/server
优点:该结构的软件,只开发服务器端,不开发客户端,因为客户端直接由浏览器取代。
开发成本相对低,维护更为简单。
缺点:所有运算都要在服务端完成。