socket编程是网络通信的一个基础应用,无论是手机端还是PC端都需要socket技术来建立网络通信。在本章小编主要从以下几个方面来介绍socket的相关知识:
分别是“什么是socket?”,“socket有什么特点?”,“socket与Http以及TCP的区别”,“移动端socket的Demo”。写的不好的地方请大家批评指正。
一、何为socket?
socket也被称为“套接字”,它是一种网络通信的方式,它不是一种协议,而是提供给程序员实现TCP/IP封装与数据传输的接口。用于在客户端和服务端之间建立一个通信管道,使得应用程序/客户端可以通过这个通信管道向服务器/另一台主机发送请求,同时服务器也可以进行相应,建立两者之间的数据传输与交换。在Android中,我们知道每个应用程序都可以使用多线程来进行操作,用于网络传输的app的多个线程可以都建立一个socket连接,每个线程维护自己的socket管道,从而实现并发的网络通信。
socket的组成部分主要包括5个:连接使用的协议(TCP/UDP),客户端IP,客户端端口号,服务器端IP地址,服务器端的端口号。在Android中,serverSocket用于服务器端而socket是用于建立网络连接时用的,在连接成功时两端都会产生一个socket对象。
二、socket的特点及通信原理
我们可以看到,一个socket建立时所使用的协议可以是TCP的,也可以是UDP的。TCP是可靠的而UDP是不可靠的,原因就是TCP建立时需要三次握手,双方建立通信管道才可以数据传输,且是双向的。而UDP在数据传输时只是把应用层发来的数据进行打包并且附上目的的地址,不负责对方是否接收到。
一个Socket的通信建立可以分为客户端和服务端两部分:
服务端:1、创建服务器套接字并绑定到自己的一个端口上,这个端口最好大于1024,因为0~1023端口号是被系统预留的,会被一些系统功能所调用。
2、建立套接字的监听器,监听是否有别的客户端程序来请求访问本端口的serverSocket。
3、如果接受到,则接受请求建立连接通信
客户端:1、创建一个客户端套接字,绑定要访问的目标服务器的IP地址及端口号
2、连接服务端(这里与PC上的socket连接不同,不需要connect(),因为Android上的客户端socket在创建时,如果创建成功会自动内部调用连接方法)
3、与服务端进行通信
4、主动关闭客户端socket
了解了客户端与服务端的通信过程,我们都能了解到:服务端其实一直保持着监听客户端请求的状态,socket连接只能由发起请求的客户端主动关闭。两端在进行通信的时候,数据是通过InputStream和OutputStream来实现的,且首先是服务端收到输入流,再将结果通过输出流返回给客户端。而客户端方面是先发送输出流,再接收到输入流。
大致模式如下:
服务器和客户端的连接:服务器监听------->客户端请求------->建立连接
三、socket与HTTP、TCP的区别
首先需要知道HTTP是应用层协议,主要解决的是如何包装数据。而TCP是传输层协议,主要解决的是如何传输数据。socket是TCP/IP协议的封装,本身并不是协议。
socket的连接模式与HTTP、TCP的连接模式不一致。从上节已经了解到socket的连接是从服务器监听开始的,而在TCP方面,客户端与服务器的连接需要经过三次握手之后才正式开始数据的传输,而HTTP则是基于"请求-响应"才能建立数据传输,什么意思呢?只有先请求,服务器才能对外发数据,不能主动建立传输。在HTTP经过了0.9、1.0和1.1时代,这种连接模式可以在一次TCP连接建立时一直保持,响应多次请求。
当socket是传输层的一个封装,传输层主要由TCP和UDP协议,当socket使用的是TCP协议而不是UDP协议时,一个socket连接其实就是TCP连接。
四、Demo
这里我们将创建一个demo,在Android上使用socket。我们的Demo主要功能是在编辑框中编写一段文字,然后按发送按钮,最后内部实现通过socket的通信方式,反馈到textview上进行显示编辑的内容。里面的细节重点会在末尾处分析。
下面是xml布局文件:
android:id="@+id/btn_conn"/> <EditText android:layout_width="fill_parent" android:layout_height="wrap_content" android:hint="请在这里编辑要发送的内容" android:id="@+id/et_message"/> <Button android:layout_width="fill_parent" android:layout_height="wrap_content" android:text="发送" android:id="@+id/btn_sent"/> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:id="@+id/tv" android:hint="接收发来的数据"/> </LinearLayout>
布局效果如下:
首先来描述下我们要实现的功能逻辑:
1、通过点击连接先建立socket通信管道
2、在编辑框中输入我们要发送的内容,按下发送按钮,数据会显示到文本控件中;
3、在内部自己建立一个线程,不能在UI线程中更新,会造成ANR;
4、本身即作为发送方,又作为接收方,主要过程是把数据打包发送,通过outputstream进行输出传输,创建为消息的内容;接着被handler收到,扔进消息队列,处理的时候再把数据解压,进行ui更新。主要功能handler.sentmessage()和handler.handmessage()。还有就是outputstream和inputstream时的数据打包,encoding必须一致。中文可以使用GB2312.
代码如下:
package com.example.mysocketdemo; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.Socket; import java.net.UnknownHostException; import android.app.Activity; import android.app.ActionBar; import android.app.Fragment; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Editable; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import android.widget.Toast; import android.os.Build; public class MainActivity extends Activity { private TextView tv; private Button btnsent,btnconn; private EditText ed_message; private OutputStream output; private Socket clientSocket; private Handler mHandler; private MyThread mythread; boolean stop = true; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState == null) { getFragmentManager().beginTransaction() .add(R.id.container, new PlaceholderFragment()) .commit(); } init(); //onClickEvent---connect btnconn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub try { clientSocket = new Socket("127.0.0.1", 8888); } catch (UnknownHostException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Toast.makeText(getApplicationContext(), "connect ok", Toast.LENGTH_SHORT).show(); //把socket绑定到自己的线程对象 try { mythread = new MyThread(clientSocket); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } mythread.start();//启动线程 //更新UI,大部分的数据工作已经交给了mythread对象 btnsent.setEnabled(true); btnconn.setEnabled(false); stop = false; } }); //sent Message btnsent.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub //当点击按钮时,会获取编辑框中的数据,然后提交给线程 //先将发送内容进行打包 byte[] msgBuffer = null; try { msgBuffer = ed_message.getText().toString().getBytes("GB2312"); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } //打包完成之后,添加socket的输出流对象 try { output = clientSocket.getOutputStream(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { //输出流对象将字节写入 //无论是输出流还是输入流,操作的都是字节,如果向变成字符串,需要自己构建String对象 output.write(msgBuffer); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Toast.makeText(getApplicationContext(), "发送成功", Toast.LENGTH_SHORT).show(); ed_message.setText(""); } }); //在连接和发送数据之后,接下来就是处理了,发送的数据会通过message的方式传递到消息队列,再由handl进行获取 mHandler = new Handler(){ public void handleMessage(android.os.Message msg) { tv.setText(msg.obj.toString()); }; }; } public void init() { tv = (TextView) findViewById(R.id.tv); btnsent = (Button) findViewById(R.id.btn_sent); ed_message = (EditText) findViewById(R.id.et_message); btnconn = (Button) findViewById(R.id.btn_conn); btnconn.setEnabled(true); btnsent.setEnabled(false); } //自定义线程类; private class MyThread extends Thread{ //构建自己的socket对象,用来在线程内使用 private Socket socket; private byte[] buf = null; private InputStream inputstream; String str = null; public MyThread(Socket socket) throws IOException { this.socket = socket; inputstream = this.socket.getInputStream(); } @Override public void run() { // TODO Auto-generated method stub while(!stop) { buf = new byte[512]; //将inputstream内的数据读到buf里 try { this.inputstream.read(buf); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } try { //将buf里的字符流进行解析,得到 this.str = new String(buf, "GB2312").trim(); } catch (UnsupportedEncodingException e) { // TODO Auto-generated catch block e.printStackTrace(); } //线程内获取了来自socket的Inputstream字节流,并且转换成字符串后,线程就获取了消息的实体内容 //此时线程将执行自己的一个使命,就是创建消息,并发送出去 Message msg = new Message(); msg.obj = this.str; mHandler.sendMessage(msg); } } } @Override protected void onDestroy() { // TODO Auto-generated method stub if (mythread!=null) { mythread.interrupt(); } super.onDestroy(); }
了解代码的逻辑之后,我再理一下整个思路:
当我们点击了connnet之后,就会创建连接目标ip地址及端口的socket连接,此时,什么数据都没有,socket的Outputstream和InputStream里面都是空的。尽管我们自定义的线程在连接后就启动了,并且必须执行run方法,但是此时没有数据来接收。
当我们编辑好文笔点击了sent按钮之后,编辑框里的文本会由字符串向字节的包装,使用的是String.getbyte()方法,为啥呢,因为outputstream和inputstream只接受字节流的数据。当socket的getOutputStream获取了outputstream对象之后,执行了write方法进行数据的写入,而此时线程依然在执行,它突然发现连接的socket里有数据了,就使用getIupteStream获取了inputstream对象,采用了read方法读取了字节流,最后打包成字符串。
打包成字符串之后,线程要执行一个自己的使命,就是将其封装成消息,以消息的形式塞给主线程的消息队列,此时UI线程的使者Hanlder就完成了sendmessage的任务。
最后回到主线程,Handler对象使用handlmessage方法将消息的内容显示在了textView上。
以上就是整个逻辑。
版权声明:本文为博主原创文章,未经博主允许不得转载。