1.OSI七层网络模型浅析
OSI七层网络模型(从下往上):
**物理层(Physical):**设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的 环境。可以理解为网络传输的物理媒体部分,比如网卡,网线,集线器,中继器,调制解调器等! 在这一层,数据还没有被组织,仅作为原始的位流或电气电压处理,这一层的单位是:bit比特
**数据链路层(Datalink):**可以理解为数据通道,主要功能是如何在不可靠的物理线路上进行 数据的可靠传递,改层作用包括:物理地址寻址,数据的成帧,流量控制,数据检错以及重发等! 另外这个数据链路指的是:物理层要为终端设备间的数据通信提供传输媒体及其连接。媒体是 长期的,连接是有生存期的。在连接生存期内,收发两端可以进行不等的一次或多次数据通信。 每次通信都要经过建立通信联络和拆除通信联络两过程!这种建立起来的数据收发关系~ 该层的设备有:网卡,网桥,网路交换机,另外该层的单位为:帧
**网络层(Network):**主要功能是将网络地址翻译成对应的物理地址,并决定如何将数据从发 送方路由到接收方,所谓的路由与寻径:一台终端可能需要与多台终端通信,这样就产生的了 把任意两台终端设备数据链接起来的问题!简单点说就是:建立网络连接和为上层提供服务! 该层的设备有:路由!该层的单位为:数据包,另外IP协议就在这一层!
**传输层(Transport):**向上面的应用层提供通信服务,面向通信部分的最高层,同时也是 用户功能中的最低层。接收会话层数据,在必要时将数据进行分割,并将这些数据交给网络 层,并且保证这些数据段有效的到达对端!所以这层的单位是:数据段;而这层有两个很重要 的协议就是:TCP传输控制协议与UDP用户数据报协议,这也是本章节核心讲解的部分!
**会话层(Session):**负责在网络中的两节点之间建立、维持和终止通信。建立通信链接, 保持会话过程通信链接的畅通,同步两个节点之间的对话,决定通信是否被中断以及通信中断时 决定从何处重新发送,即不同机器上的用户之间会话的建立及管理!
**表示层(Presentation):**对来自应用层的命令和数据进行解释,对各种语法赋予相应 的含义,并按照一定的格式传送给会话层。其主要功能是"处理用户信息的表示问题,如编码、 数据格式转换和加密解密,压缩解压缩"等
应用层(Application): OSI参考模型的最高层,为用户的应用程序提供网络服务。 它在其他6层工作的基础上,负责完成网络中应用程序与网络操作系统之间的联系,建立与结束使用者之间的联系,并完成网络用户提出的各种网络服务及应用所需的监督、管理和服务等各种协议。此外,该层还负责协调各个应用程序间的工作。应用层为用户提供的服务和协议有:文件服务、目录服务、文件传输服务(FTP)、远程登录服务(Telnet)、电子邮件服务(E-mail)、打印服务、安全服务、网络管理服务、数据库服务等。
上面我们浅述了OSI七层网络模型,下面总结下:
OSI是一个理想的模型,一般的网络系统只涉及其中的几层,在七层模型中,每一层都提供一个特殊 的网络功能,从网络功能角度观察:
下面4层(物理层、数据链路层、网络层和传输层)主要提供数据传输和交换功能, 即以节点到节点之间的通信为主
第4层作为上下两部分的桥梁,是整个网络体系结构中最关键的部分;
上3层(会话层、表示层和应用层)则以提供用户与应用程序之间的信息和数据处理功能为主。
简言之,下4层主要完成通信子网的功能,上3层主要完成资源子网的功能。
2.TCP/IP四层模型
TCP/IP是一组协议的代名词,它还包括许多协议,组成了TCP/IP协议簇。 TCP/IP协议簇分为四层,IP位于协议簇的第二层(对应OSI的第三层),TCP位于协议簇的第三层 (对应OSI的第四层)。TCP/IP通讯协议采用了4层的层级结构,每一层都呼叫它的下一层所提供 的网络来完成自己的需求。这4层分别为:
**应用层:**应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、 网络远程访问协议(Telnet)等。
**传输层:**在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、 用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中, 这一层负责传送数据,并且确定数据已被送达并接收。
**网络互连层:**负责提供基本的数据封包传送功能,让每一块数据包都能够到达目 的主机(但不检查是否被正确接收),如网际协议(IP)。
**主机到网络层:**对实际的网络媒体的管理,定义如何使用实际网络 (如Ethernet、Serial Line等)来传送数据。
3.TCP/UDP区别讲解
好吧,前两点侃侃而谈,只是给大家普及下OSI七层模型和TCP/IP四层模型的概念,接下来要讲的是 和我们Socket开发相关的一些概念名词了!
1)IP地址
2)端口
- 用于区分不同的应用程序
- 端口号的范围为0-65535,其中0-1023未系统的保留端口,我们的程序尽可能别使用这些端口!
- IP地址和端口号组成了我们的Socket,Socket是网络运行程序间双向通信链路的终结点, 是TCP和UDP的基础!
- 常用协议使用的端口:HTTP:80,FTP:21,TELNET:23
3)TCP协议与UDP协议的比较:
TCP协议流程详解:
首先TCP/IP是一个协议簇,里面包括很多协议的。UDP只是其中的一个。之所以命名为TCP/IP协议, 因为TCP,IP协议是两个很重要的协议,就用他两命名了。
下面我们来讲解TCP协议和UDP协议的区别:
TCP(Transmission Control Protocol,传输控制协议)是面向连接的协议,即在收发数据钱 ,都需要与对面建立可靠的链接,这也是面试经常会问到的TCP的三次握手以及TCP的四次挥手!
三次握手: 建立一个TCP连接时,需要客户端和服务端总共发送3个包以确认连接的建立, 在Socket编程中,这一过程由客户端执行connect来触发,具体流程图如下:
第一次握手: Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server, Client进入SYN_SENT状态,等待Server确认。
第二次握手: Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位 SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求 ,Server进入SYN_RCVD状态。
第三次握手: Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK 置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则 连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以 开始传输数据了。
四次挥手: 终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。 在Socket编程中,这一过程由客户端或服务端任一方执行close来触发,具体流程图如下:
第一次挥手: Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入 FIN_WAIT_1状态
第二次挥手: Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同, 一个FIN占用一个序号),Server进入CLOSE_WAIT状态。
第三次挥手: Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK 状态。
第四次挥手: Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
另外也可能是同时发起主动关闭的情况:
另外还可能有一个常见的问题就是:为什么建立连接是三次握手,而关闭连接却是四次挥手呢? 答:因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里 发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还 能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些 数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会 分开发送。
UDP协议详解:
UDP(User Datagram Protocol)用户数据报协议,非连接的协议,传输数据之前源端和终端不 建立连接,当它想传送时就简单地去抓取来自应用程序的数据,并尽可能快地把它扔到网络上。 在发送端,UDP传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力和传输带宽 的限制;在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读一个消息段。 相比TCP就是无需建立链接,结构简单,无法保证正确性,容易丢包
4.Java中对于网络提供的几个关键类:
针对不同的网络通信层次,Java给我们提供的网络功能有四大类:
InetAddress: 用于标识网络上的硬件资源
URL: 统一资源定位符,通过URL可以直接读取或者写入网络上的数据
Socket和ServerSocket: 使用TCP协议实现网络通信的Socket相关的类
Datagram: 使用UDP协议,将数据保存在数据报中,通过网络进行通信
本节我们只介绍前两个类,Socket与Datagram到TCP和UDP的章节再讲解!
~InetAddress的使用例子:
示例代码:
public class InetAddressTest {
public static void main(String[] args) throws Exception{
//获取本机InetAddress的实例:
InetAddress address = InetAddress.getLocalHost();
System.out.println("本机名:" + address.getHostName());
System.out.println("IP地址:" + address.getHostAddress());
byte[] bytes = address.getAddress();
System.out.println("字节数组形式的IP地址:" + Arrays.toString(bytes));
System.out.println("直接输出InetAddress对象:" + address);
}
}
5.什么是Socket?
6.Socket通信模型:
Socket通信实现步骤解析:
Step 1:创建ServerSocket和Socket
Step 2:打开连接到的Socket的输入/输出流
Step 3:按照协议对Socket进行读/写操作
Step 4:关闭输入输出流,以及Socket
好的,我们接下来写一个简单的例子,开启服务端后,客户端点击按钮然后链接服务端, 并向服务端发送一串字符串,表示通过Socket链接上服务器~
7.Socket服务端的编写:
服务端要做的事有这些:
Step 1:创建ServerSocket对象,绑定监听的端口
Step 2:调用accept()方法监听客户端的请求
Step 3:连接建立后,通过输入流读取客户端发送的请求信息
Step 4:通过输出流向客户端发送响应信息
Step 5:关闭相关资源
代码实现:
直接在Eclipse下创建一个Java项目,然后把Java代码贴进去即可!
public class SocketServer {
public static void main(String[] args) throws IOException {
//1.创建一个服务器端Socket,即ServerSocket,指定绑定的端口,并监听此端口
ServerSocket serverSocket = new ServerSocket(12345);
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
Socket socket = null;
//2.调用accept()等待客户端连接
System.out.println("~~~服务端已就绪,等待客户端接入~,服务端ip地址: " + ip);
socket = serverSocket.accept();
//3.连接后获取输入流,读取客户端信息
InputStream is=null;
InputStreamReader isr=null;
BufferedReader br=null;
OutputStream os=null;
PrintWriter pw=null;
is = socket.getInputStream(); //获取输入流
isr = new InputStreamReader(is,"UTF-8");
br = new BufferedReader(isr);
String info = null;
while((info=br.readLine())!=null){//循环读取客户端的信息
System.out.println("客户端发送过来的信息" + info);
}
socket.shutdownInput();//关闭输入流
socket.close();
}
}
然后我们把代码run起来,控制台会打印:
好的,接下来到Android客户端了!
8.Socket客户端的编写:
客户端要做的事有这些:
Step 1:创建Socket对象,指明需要链接的服务器的地址和端号
Step 2:链接建立后,通过输出流向服务器发送请求信息
Step 3:通过输出流获取服务器响应的信息
Step 4:关闭相关资源
代码实现:
MainActivity.java:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button btn_accept = (Button) findViewById(R.id.btn_accept);
btn_accept.setOnClickListener(this);
}
@Override
public void onClick(View v) {
new Thread() {
@Override
public void run() {
try {
acceptServer();
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
}
private void acceptServer() throws IOException {
//1.创建客户端Socket,指定服务器地址和端口
Socket socket = new Socket("172.16.2.54", 12345);
//2.获取输出流,向服务器端发送信息
OutputStream os = socket.getOutputStream();//字节输出流
PrintWriter pw = new PrintWriter(os);//将输出流包装为打印流
//获取客户端的IP地址
InetAddress address = InetAddress.getLocalHost();
String ip = address.getHostAddress();
pw.write("客户端:~" + ip + "~ 接入服务器!!");
pw.flush();
socket.shutdownOutput();//关闭输出流
socket.close();
}
}
因为Android不允许在主线程(UI线程)中做网络操作,所以这里需要我们自己 另开一个线程来连接Socket!
运行结果:
点击按钮后,服务端控制台打印:
9.增强版案例:小猪简易聊天室
只是点击个按钮,然后服务器返回一串信息,肯定是很无趣的是吧,接下来我们来 搭建一个超简单的聊天室,我们需要用到线程池,存储Socket链接的集合,我们还需要 字节写一个线程,具体的我们在代码中来体会!
实现的效果图:
先把我们的服务端跑起来:
接着把我们的程序分别跑到两台模拟器上:
接下来我们来写代码:
首先是服务端,就是将读写socket的操作放到自定义线程当中,创建ServerSocket后,循环 调用accept方法,当有新客户端接入,将socket加入集合当中,同时在线程池新建一个线程!
另外,在读取信息的方法中,对输入字符串进行判断,如果为bye字符串,将socket从集合中 移除,然后close掉!
Server.java:
public class Server {
//定义相关的参数,端口,存储Socket连接的集合,ServerSocket对象
//以及线程池
private static final int PORT = 12345;
private List<Socket> mList = new ArrayList<Socket>();
private ServerSocket server = null;
private ExecutorService myExecutorService = null;
public static void main(String[] args) {
new Server();
}
public Server()
{
try
{
server = new ServerSocket(PORT);
//创建线程池
myExecutorService = Executors.newCachedThreadPool();
System.out.println("服务端运行中...\n");
Socket client = null;
while(true)
{
client = server.accept();
mList.add(client);
myExecutorService.execute(new Service(client));
}
}catch(Exception e){e.printStackTrace();}
}
class Service implements Runnable
{
private Socket socket;
private BufferedReader in = null;
private String msg = "";
public Service(Socket socket) {
this.socket = socket;
try
{
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
msg = "用户:" +this.socket.getInetAddress() + "~加入了聊天室"
+"当前在线人数:" +mList.size();
this.sendmsg();
}catch(IOException e){e.printStackTrace();}
}
@Override
public void run() {
try{
while(true)
{
if((msg = in.readLine()) != null)
{
if(msg.equals("bye"))
{
System.out.println("~~~~~~~~~~~~~");
mList.remove(socket);
in.close();
msg = "用户:" + socket.getInetAddress()
+ "退出:" +"当前在线人数:"+mList.size();
socket.close();
this.sendmsg();
break;
}else{
msg = socket.getInetAddress() + " 说: " + msg;
this.sendmsg();
}
}
}
}catch(Exception e){e.printStackTrace();}
}
//为连接上服务端的每个客户端发送信息
public void sendmsg()
{
System.out.println(msg);
int num = mList.size();
for(int index = 0;index < num;index++)
{
Socket mSocket = mList.get(index);
PrintWriter pout = null;
try {
pout = new PrintWriter(new BufferedWriter(
new OutputStreamWriter(mSocket.getOutputStream(),"UTF-8")),true);
pout.println(msg);
}catch (IOException e) {e.printStackTrace();}
}
}
}
}
接着到客户端,客户端的难点在于要另外开辟线程的问题,因为Android不允许直接在 主线程中做网络操作,而且不允许在主线程外的线程操作UI,这里的做法是自己新建 一个线程,以及通过Hanlder来更新UI,实际开发不建议直接这样做!!!
布局文件:activity_main.xml:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="小猪简易聊天室" />
<TextView
android:id="@+id/txtshow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<EditText
android:id="@+id/editsend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<Button
android:id="@+id/btnsend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="发送"
/>
</LinearLayout>
MainActivity.java:
public class MainActivity extends AppCompatActivity implements Runnable {
//定义相关变量,完成初始化
private TextView txtshow;
private EditText editsend;
private Button btnsend;
private static final String HOST = "172.16.2.54";
private static final int PORT = 12345;
private Socket socket = null;
private BufferedReader in = null;
private PrintWriter out = null;
private String content = "";
private StringBuilder sb = null;
//定义一个handler对象,用来刷新界面
public Handler handler = new Handler() {
public void handleMessage(Message msg) {
if (msg.what == 0x123) {
sb.append(content);
txtshow.setText(sb.toString());
}
}
;
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sb = new StringBuilder();
txtshow = (TextView) findViewById(R.id.txtshow);
editsend = (EditText) findViewById(R.id.editsend);
btnsend = (Button) findViewById(R.id.btnsend);
//当程序一开始运行的时候就实例化Socket对象,与服务端进行连接,获取输入输出流
//因为4.0以后不能再主线程中进行网络操作,所以需要另外开辟一个线程
new Thread() {
public void run() {
try {
socket = new Socket(HOST, PORT);
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
socket.getOutputStream())), true);
} catch (IOException e) {
e.printStackTrace();
}
}
}.start();
//为发送按钮设置点击事件
btnsend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String msg = editsend.getText().toString();
if (socket.isConnected()) {
if (!socket.isOutputShutdown()) {
out.println(msg);
}
}
}
});
new Thread(MainActivity.this).start();
}
//重写run方法,在该方法中输入流的读取
@Override
public void run() {
try {
while (true) {
if (socket.isConnected()) {
if (!socket.isInputShutdown()) {
if ((content = in.readLine()) != null) {
content += "\n";
handler.sendEmptyMessage(0x123);
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
10.使用Socket来实现大文件的断点续传
1.运行效果图:
1.先把我们编写好的Socket服务端运行起来:
2.将一个音频文件放到SD卡根目录下:
3.运行我们的客户端:
4.上传成功后可以看到我们的服务端的项目下生成一个file的文件夹,我们可以在这里找到上传的文件: .log那个是我们的日志文件
2.实现流程图:
3.代码示例:
先编写一个服务端和客户端都会用到的流解析类:
StreamTool.java:
public class StreamTool {
public static void save(File file, byte[] data) throws Exception {
FileOutputStream outStream = new FileOutputStream(file);
outStream.write(data);
outStream.close();
}
public static String readLine(PushbackInputStream in) throws IOException {
char buf[] = new char[128];
int room = buf.length;
int offset = 0;
int c;
loop: while (true) {
switch (c = in.read()) {
case -1:
case '\n':
break loop;
case '\r':
int c2 = in.read();
if ((c2 != '\n') && (c2 != -1)) in.unread(c2);
break loop;
default:
if (--room < 0) {
char[] lineBuffer = buf;
buf = new char[offset + 128];
room = buf.length - offset - 1;
System.arraycopy(lineBuffer, 0, buf, 0, offset);
}
buf[offset++] = (char) c;
break;
}
}
if ((c == -1) && (offset == 0)) return null;
return String.copyValueOf(buf, 0, offset);
}
/**
* 读取流
* @param inStream
* @return 字节数组
* @throws Exception
*/
public static byte[] readStream(InputStream inStream) throws Exception{
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while( (len=inStream.read(buffer)) != -1){
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
return outSteam.toByteArray();
}
}
1)服务端的实现:
socket管理与多线程管理类:
FileServer.java:
public class FileServer {
private ExecutorService executorService;//线程池
private int port;//监听端口
private boolean quit = false;//退出
private ServerSocket server;
private Map<Long, FileLog> datas = new HashMap<Long, FileLog>();//存放断点数据
public FileServer(int port){
this.port = port;
//创建线程池,池中具有(cpu个数*50)条线程
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 50);
}
/**
* 退出
*/
public void quit(){
this.quit = true;
try {
server.close();
} catch (IOException e) {
}
}
/**
* 启动服务
* @throws Exception
*/
public void start() throws Exception{
server = new ServerSocket(port);
while(!quit){
try {
Socket socket = server.accept();
//为支持多用户并发访问,采用线程池管理每一个用户的连接请求
executorService.execute(new SocketTask(socket));
} catch (Exception e) {
// e.printStackTrace();
}
}
}
private final class SocketTask implements Runnable{
private Socket socket = null;
public SocketTask(Socket socket) {
this.socket = socket;
}
public void run() {
try {
System.out.println("accepted connection "+ socket.getInetAddress()+ ":"+ socket.getPort());
PushbackInputStream inStream = new PushbackInputStream(socket.getInputStream());
//得到客户端发来的第一行协议数据:Content-Length=143253434;filename=xxx.3gp;sourceid=
//如果用户初次上传文件,sourceid的值为空。
String head = StreamTool.readLine(inStream);
System.out.println(head);
if(head!=null){
//下面从协议数据中提取各项参数值
String[] items = head.split(";");
String filelength = items[0].substring(items[0].indexOf("=")+1);
String filename = items[1].substring(items[1].indexOf("=")+1);
String sourceid = items[2].substring(items[2].indexOf("=")+1);
long id = System.currentTimeMillis();//生产资源id,如果需要唯一性,可以采用UUID
FileLog log = null;
if(sourceid!=null && !"".equals(sourceid)){
id = Long.valueOf(sourceid);
log = find(id);//查找上传的文件是否存在上传记录
}
File file = null;
int position = 0;
if(log==null){//如果不存在上传记录,为文件添加跟踪记录
String path = new SimpleDateFormat("yyyy/MM/dd/HH/mm").format(new Date());
File dir = new File("file/"+ path);
if(!dir.exists()) dir.mkdirs();
file = new File(dir, filename);
if(file.exists()){//如果上传的文件发生重名,然后进行改名
filename = filename.substring(0, filename.indexOf(".")-1)+ dir.listFiles().length+ filename.substring(filename.indexOf("."));
file = new File(dir, filename);
}
save(id, file);
}else{// 如果存在上传记录,读取已经上传的数据长度
file = new File(log.getPath());//从上传记录中得到文件的路径
if(file.exists()){
File logFile = new File(file.getParentFile(), file.getName()+".log");
if(logFile.exists()){
Properties properties = new Properties();
properties.load(new FileInputStream(logFile));
position = Integer.valueOf(properties.getProperty("length"));//读取已经上传的数据长度
}
}
}
OutputStream outStream = socket.getOutputStream();
String response = "sourceid="+ id+ ";position="+ position+ "\r\n";
//服务器收到客户端的请求信息后,给客户端返回响应信息:sourceid=1274773833264;position=0
//sourceid由服务器端生成,唯一标识上传的文件,position指示客户端从文件的什么位置开始上传
outStream.write(response.getBytes());
RandomAccessFile fileOutStream = new RandomAccessFile(file, "rwd");
if(position==0) fileOutStream.setLength(Integer.valueOf(filelength));//设置文件长度
fileOutStream.seek(position);//指定从文件的特定位置开始写入数据
byte[] buffer = new byte[1024];
int len = -1;
int length = position;
while( (len=inStream.read(buffer)) != -1){//从输入流中读取数据写入到文件中
fileOutStream.write(buffer, 0, len);
length += len;
Properties properties = new Properties();
properties.put("length", String.valueOf(length));
FileOutputStream logFile = new FileOutputStream(new File(file.getParentFile(), file.getName()+".log"));
properties.store(logFile, null);//实时记录已经接收的文件长度
logFile.close();
}
if(length==fileOutStream.length()) delete(id);
fileOutStream.close();
inStream.close();
outStream.close();
file = null;
}
} catch (Exception e) {
e.printStackTrace();
}finally{
try {
if(socket!=null && !socket.isClosed()) socket.close();
} catch (IOException e) {}
}
}
}
public FileLog find(Long sourceid){
return datas.get(sourceid);
}
//保存上传记录
public void save(Long id, File saveFile){
//日后可以改成通过数据库存放
datas.put(id, new FileLog(id, saveFile.getAbsolutePath()));
}
//当文件上传完毕,删除记录
public void delete(long sourceid){
if(datas.containsKey(sourceid)) datas.remove(sourceid);
}
private class FileLog{
private Long id;
private String path;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public FileLog(Long id, String path) {
this.id = id;
this.path = path;
}
}
}
服务端界面类:ServerWindow.java:
public class ServerWindow extends Frame {
private FileServer s = new FileServer(12345);
private Label label;
public ServerWindow(String title) {
super(title);
label = new Label();
add(label, BorderLayout.PAGE_START);
label.setText("服务器已经启动");
this.addWindowListener(new WindowListener() {
public void windowOpened(WindowEvent e) {
new Thread(new Runnable() {
public void run() {
try {
s.start();
} catch (Exception e) {
// e.printStackTrace();
}
}
}).start();
}
public void windowIconified(WindowEvent e) {
}
public void windowDeiconified(WindowEvent e) {
}
public void windowDeactivated(WindowEvent e) {
}
public void windowClosing(WindowEvent e) {
s.quit();
System.exit(0);
}
public void windowClosed(WindowEvent e) {
}
public void windowActivated(WindowEvent e) {
}
});
}
/**
* @param args
*/
public static void main(String[] args) throws IOException {
InetAddress address = InetAddress.getLocalHost();
ServerWindow window = new ServerWindow("文件上传服务端:" + address.getHostAddress());
window.setSize(400, 300);
window.setVisible(true);
}
}
2)客户端(Android端)
首先是布局文件:activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical"
android:padding="5dp">
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="文件名"
android:textSize="18sp" />
<EditText
android:id="@+id/edit_fname"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Nikki Jamal - Priceless.mp3" />
<Button
android:id="@+id/btn_upload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上传" />
<Button
android:id="@+id/btn_stop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="停止" />
<ProgressBar
android:id="@+id/pgbar"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="40px" />
<TextView
android:id="@+id/txt_result"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center" />
</LinearLayout>
因为断点续传,我们需要保存上传的进度,我们需要用到数据库,这里我们定义一个数据库 管理类:DBOpenHelper.java:
/**
* Created by Jay on 2015/9/17 0017.
*/
public class DBOpenHelper extends SQLiteOpenHelper {
public DBOpenHelper(Context context) {
super(context, "jay.db", null, 1);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE IF NOT EXISTS uploadlog (_id integer primary key autoincrement, path varchar(20), sourceid varchar(20))");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
然后是数据库操作类:UploadHelper.java:
/**
* Created by Jay on 2015/9/17 0017.
*/
public class UploadHelper {
private DBOpenHelper dbOpenHelper;
public UploadHelper(Context context) {
dbOpenHelper = new DBOpenHelper(context);
}
public String getBindId(File file) {
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = db.rawQuery("select sourceid from uploadlog where path=?", new String[]{file.getAbsolutePath()});
if (cursor.moveToFirst()) {
return cursor.getString(0);
}
return null;
}
public void save(String sourceid, File file) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
db.execSQL("insert into uploadlog(path,sourceid) values(?,?)",
new Object[]{file.getAbsolutePath(), sourceid});
}
public void delete(File file) {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
db.execSQL("delete from uploadlog where path=?", new Object[]{file.getAbsolutePath()});
}
}
对了,别忘了客户端也要贴上那个流解析类哦,最后就是我们的MainActivity.java了:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText edit_fname;
private Button btn_upload;
private Button btn_stop;
private ProgressBar pgbar;
private TextView txt_result;
private UploadHelper upHelper;
private boolean flag = true;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
pgbar.setProgress(msg.getData().getInt("length"));
float num = (float) pgbar.getProgress() / (float) pgbar.getMax();
int result = (int) (num * 100);
txt_result.setText(result + "%");
if (pgbar.getProgress() == pgbar.getMax()) {
Toast.makeText(MainActivity.this, "上传成功", Toast.LENGTH_SHORT).show();
}
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindViews();
upHelper = new UploadHelper(this);
}
private void bindViews() {
edit_fname = (EditText) findViewById(R.id.edit_fname);
btn_upload = (Button) findViewById(R.id.btn_upload);
btn_stop = (Button) findViewById(R.id.btn_stop);
pgbar = (ProgressBar) findViewById(R.id.pgbar);
txt_result = (TextView) findViewById(R.id.txt_result);
btn_upload.setOnClickListener(this);
btn_stop.setOnClickListener(this);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_upload:
String filename = edit_fname.getText().toString();
flag = true;
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
File file = new File(Environment.getExternalStorageDirectory(), filename);
if (file.exists()) {
pgbar.setMax((int) file.length());
uploadFile(file);
} else {
Toast.makeText(MainActivity.this, "文件并不存在~", Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(MainActivity.this, "SD卡不存在或者不可用", Toast.LENGTH_SHORT).show();
}
break;
case R.id.btn_stop:
flag = false;
break;
}
}
private void uploadFile(final File file) {
new Thread(new Runnable() {
public void run() {
try {
String sourceid = upHelper.getBindId(file);
Socket socket = new Socket("172.16.2.54", 12345);
OutputStream outStream = socket.getOutputStream();
String head = "Content-Length=" + file.length() + ";filename=" + file.getName()
+ ";sourceid=" + (sourceid != null ? sourceid : "") + "\r\n";
outStream.write(head.getBytes());
PushbackInputStream inStream = new PushbackInputStream(socket.getInputStream());
String response = StreamTool.readLine(inStream);
String[] items = response.split(";");
String responseSourceid = items[0].substring(items[0].indexOf("=") + 1);
String position = items[1].substring(items[1].indexOf("=") + 1);
if (sourceid == null) {//如果是第一次上传文件,在数据库中不存在该文件所绑定的资源id
upHelper.save(responseSourceid, file);
}
RandomAccessFile fileOutStream = new RandomAccessFile(file, "r");
fileOutStream.seek(Integer.valueOf(position));
byte[] buffer = new byte[1024];
int len = -1;
int length = Integer.valueOf(position);
while (flag && (len = fileOutStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
length += len;//累加已经上传的数据长度
Message msg = new Message();
msg.getData().putInt("length", length);
handler.sendMessage(msg);
}
if (length == file.length()) upHelper.delete(file);
fileOutStream.close();
outStream.close();
inStream.close();
socket.close();
} catch (Exception e) {
Toast.makeText(MainActivity.this, "上传异常~", Toast.LENGTH_SHORT).show();
}
}
}).start();
}
}
最后,还有,记得往AndroidManifest.xml中写入这些权限哦!
<!-- 在SDCard中创建与删除文件权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
<!-- 往SDCard写入数据权限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 访问internet权限 -->
<uses-permission android:name="android.permission.INTERNET"/>
11.基于UDP协议的Socket通信
TCP和UDP最大的区别在于是否需要客户端与服务端建立连接后才能进行 数据传输,如果你学了前两节TCP的,传输前先开服务端,accept,等客户端接入,然后获得 客户端socket然后进行IO操作,而UDP则不用,UDP以数据报作为数据的传输载体,在进行传输时 首先要把传输的数据定义成数据报(Datagram),在数据报中指明数据要到达的Socket(主机地址 和端口号),然后再将数据以数据报的形式发送出去,然后就没有然后了,服务端收不收到我就 不知道了,除非服务端收到后又给我回一段确认的数据报
1.服务端实现步骤:
Step 1:创建DatagramSocket,指定端口号
Step 2:创建DatagramPacket
Step 3:接收客户端发送的数据信息
Step 4:读取数据
示例代码:
public class UPDServer {
public static void main(String[] args) throws IOException {
/*
* 接收客户端发送的数据
*/
// 1.创建服务器端DatagramSocket,指定端口
DatagramSocket socket = new DatagramSocket(12345);
// 2.创建数据报,用于接收客户端发送的数据
byte[] data = new byte[1024];// 创建字节数组,指定接收的数据包的大小
DatagramPacket packet = new DatagramPacket(data, data.length);
// 3.接收客户端发送的数据
System.out.println("****服务器端已经启动,等待客户端发送数据");
socket.receive(packet);// 此方法在接收到数据报之前会一直阻塞
// 4.读取数据
String info = new String(data, 0, packet.getLength());
System.out.println("我是服务器,客户端说:" + info);
/*
* 向客户端响应数据
*/
// 1.定义客户端的地址、端口号、数据
InetAddress address = packet.getAddress();
int port = packet.getPort();
byte[] data2 = "欢迎您!".getBytes();
// 2.创建数据报,包含响应的数据信息
DatagramPacket packet2 = new DatagramPacket(data2, data2.length, address, port);
// 3.响应客户端
socket.send(packet2);
// 4.关闭资源
socket.close();
}
}
2.客户端实现步骤:
Step 1:定义发送信息
Step 2:创建DatagramPacket,包含将要发送的信息
Step 3:创建DatagramSocket
Step 4:发送数据
public class UDPClient {
public static void main(String[] args) throws IOException {
/*
* 向服务器端发送数据
*/
// 1.定义服务器的地址、端口号、数据
InetAddress address = InetAddress.getByName("localhost");
int port = 8800;
byte[] data = "用户名:admin;密码:123".getBytes();
// 2.创建数据报,包含发送的数据信息
DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
// 3.创建DatagramSocket对象
DatagramSocket socket = new DatagramSocket();
// 4.向服务器端发送数据报
socket.send(packet);
/*
* 接收服务器端响应的数据
*/
// 1.创建数据报,用于接收服务器端响应的数据
byte[] data2 = new byte[1024];
DatagramPacket packet2 = new DatagramPacket(data2, data2.length);
// 2.接收服务器响应的数据
socket.receive(packet2);
// 3.读取数据
String reply = new String(data2, 0, packet2.getLength());
System.out.println("我是客户端,服务器说:" + reply);
// 4.关闭资源
socket.close();
}
}
本节内容比较简单,无非就是将数据转换为字节,然后放到DatagramPacket(数据报包中),发送的 时候带上接受者的IP地址和端口号,而接收时,用一个字节数组来缓存!发送的时候需要创建一个 DatagramSocket(端到端通信的类)对象,然后调用send方法给接受者发送数据报包