TCP适合传输自定义原始的字节流,比如传输一个序列化为字节流后的对象或者结构体,发送方按约定的自定义报文结构发送,接收方按约定的自定义报文解码。一种传输字流节,适合传输结构体和对象,一种传输出字符串。
package com.magcomm.net; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Reader; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketAddress; import java.net.UnknownHostException; import java.nio.CharBuffer; import org.apache.http.util.EncodingUtils; import android.util.Log; public class SocketClient { private static final String tags = "com.magcomm.nmc, SocketClient"; private Socket socket; private final String SERVER_HOST_IP = "192.168.0.15"; private final String SERVER_HOST_NAME = "mail.magcomm.cn"; private final int SERVER_HOST_PORT = 58889; private InputStream mInputStream; private OutputStream mOutputStream; private PrintWriter mPrintWriter; private BufferedReader reader; public SocketClient() { } public boolean connection() { boolean bRet = false; try { //创建一个客户端连接 InetAddress ia = InetAddress.getByName(SERVER_HOST_NAME); socket = new Socket(ia, SERVER_HOST_PORT); //socket.connect(new InetSocketAddress(SERVER_HOST_NAME, SERVER_HOST_PORT),10000); bRet = true; } catch (Exception e) { // TODO: handle exception Log.i(tags, "socket open error" + e.getMessage()); } return bRet; } //发送消息 public boolean sendMsg(String msg) { boolean bRet = false; if (socket != null) { if (socket.isConnected()) { //获取这个客户端连接SOCKET的输入输出 try { mPrintWriter = new PrintWriter(socket.getOutputStream(), true); mPrintWriter.print(msg); mPrintWriter.flush(); bRet = true; } catch (IOException e) { // TODO Auto-generated catch block Log.i(tags, "write socket error"); e.printStackTrace(); } } } return bRet; } public boolean sendMsg2(byte[] buffer, int offset, int count) { boolean bRet = false; if (socket != null) { if (socket.isConnected()) { //获取这个客户端连接SOCKET的输入输出 try { mOutputStream = socket.getOutputStream(); mOutputStream.write(buffer, offset, count); mOutputStream.flush(); Log.i("NMCDataUnPack", "socket send success--------"); bRet = true; } catch (IOException e) { // TODO Auto-generated catch block Log.i(tags, "write socket error"); e.printStackTrace(); } } } return bRet; } //读取消息 public String readMsg() { String msgString = ""; try { mInputStream = socket.getInputStream(); byte buffer[] = new byte[1024]; int reCount = mInputStream.read(buffer); msgString = EncodingUtils.getString(buffer, /*"GB2312"*/"US-ASCII"); }catch (Exception e) { Log.i(tags, "read socket error"); e.printStackTrace(); } return msgString; } public String readMsg2() { String msgString = ""; try { char[] buffer = new char[1024]; Reader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); reader.read(buffer); msgString = buffer.toString(); } catch (IOException e) { Log.i(tags, "read socket error"); e.printStackTrace(); } return msgString; } public void close() { try { mInputStream.close(); mPrintWriter.close(); mOutputStream.close(); socket.close(); } catch (Exception e) { // TODO: handle exception Log.i(tags, "socket close error" + e.getMessage()); } } }
很多地方用到多级城市选择,有人偏爱苹果的那种滚动的效果。查了下,android有类似的实现,但是实现起来比较麻烦。综合考虑下,还是使用android现有的组件来实现。
多级联动,一级操作触发其他的改变,很自然的想到用多个listview,多级和二级区别不大,因此暂时实现一个二级的城市选择,即只选择省和市,区县类似。实现的效果如下:
1、数据来源
这里选择网上得来的城市数据:中国天气网的城市数据。具体怎么完美获取最新的,今天尝试了下还是有点麻烦,之后的文章中会补上。当然最简单的方法就是下载别人弄好的。
2、数据库操作
我把数据库直接放到了assets文件夹下面,应用第一次启动的时候会拷贝城市数据库到应用的数据库目录下,然后就可以使用了。然后再封装一个数据库操作类,对其进行简单的操作,代码如下:
public class PubDBM { public static final String TABLE_CHINA_CITY_CODE = "china_city_code"; public static final String CCC_PROVINCE = "province"; // public static final String CCC_CITY = "city"; // public static final String CCC_COUNTY = "county"; // public static final String CCC_CODE = "code"; // private static PubDBM dbm; private static final String DBNAME = "china_city_code.db"; private String dbPath;// 数据库路径,不包括数据库名字 private SQLiteDatabase db = null; private static Context mContext; public static PubDBM getInstance(Context context) { mContext = context; if (dbm == null) { dbm = new PubDBM(); } return dbm; } private PubDBM() { // TODO context.getFilesDir().getPath() dbPath = "/data/data/" + mContext.getPackageName() + "/databases/"; initPublicDataBase(); } /** * 初始化数据库 */ private void initPublicDataBase() { File dbDir = new File(dbPath); if (!dbDir.exists()) { dbDir.mkdirs(); } File dbFile = new File(dbPath + DBNAME); if (!dbFile.exists()) { try { dbFile.createNewFile(); InputStream is = mContext.getResources().getAssets().open(DBNAME); OutputStream os = new FileOutputStream(dbPath + DBNAME); byte[] buffer = new byte[1024]; int length = 0; while ((length = is.read(buffer)) > 0) { os.write(buffer, 0, length); } os.flush(); os.close(); is.close(); } catch (IOException e) { e.printStackTrace(); return; } } if (db == null) { db = SQLiteDatabase.openDatabase(dbPath + DBNAME, null, SQLiteDatabase.OPEN_READONLY); } } /** * 关闭数据库 */ public void close() { if (db != null && db.isOpen()) { db.close(); } } /** * 查询所有数据 * * @return */ public Cursor queryAllData() { if (db == null) { System.out.println("db==null"); return null; } return db.rawQuery("select * from " + TABLE_CHINA_CITY_CODE + " order by " + CCC_CODE + " asc", null); } /** * 查询省 * * @return */ public Cursor queryProvinceList() { String sql = "select distinct substr(" + CCC_CODE + ",1,5) as _id," + CCC_PROVINCE; sql += " from " + TABLE_CHINA_CITY_CODE; System.out.println(sql); if (db == null) { return null; } return db.rawQuery(sql, null); } /** * 查询城市 * 能力有限,数据库查询不熟悉,结果并不是完全想要的,code对应不上 */ public Cursor queryCityList(String province) { String sql = "select * from " + TABLE_CHINA_CITY_CODE; sql += " where " + CCC_PROVINCE + " = '" + province + "' group by " + CCC_CITY; if (db == null) { return null; } return db.rawQuery(sql, null); } /** * 根据城市代码查询 * */ public Cursor queryProvinceAndCity(String cityCode) { String sql = "select " + CCC_PROVINCE + ", " + CCC_CITY; sql += " from " + TABLE_CHINA_CITY_CODE; sql += " where " + CCC_CODE + " = " + cityCode; if (db == null) { return null; } return db.rawQuery(sql, null); } }3、界面实现:
在见面上展现就是一个EditText和一个Dialog的组合,点击EditText弹出一个Dialog,选择之后Dialog消失,EditText中显示选择的结果,需要使用的地方通过EditText获取选择的值。由于很多地方带有初始数据,因此我们在界面初始化的时候可能需要显示一个之前选择的城市,这个时候我们可以调用自定义的EditText设置一个初始城市代码,这样就可以显示初始数据了。
/** * 城市选择<br> * 1、布局中添加此控件<br> * 2、拿到此控件,设置数据库对象<br> * 3、通过getCityCode()拿到选择的城市代码<br> * 4、设置默认值是通过setCityCode(String code)方法<br> * * @author ttworking * */ public class CitySelect extends EditText implements android.view.View.OnClickListener { private String province, city, code; private CitySelectDialog dialog; private PubDBM dbm; public CitySelect(Context context, AttributeSet attrs) { super(context, attrs); this.dialog = new CitySelectDialog(context); this.dbm = PubDBM.getInstance(context); this.setOnClickListener(this); setFocusable(false); setClickable(true); } /** * 获取省份加城市 * * @return 省份 城市 */ public String getProvinceCity() { return getText().toString(); } /** * 获取选择的城市代码 * * @return 城市代码 */ public String getCityCode() { return code; } /** * 设置城市信息,参数为城市代码,设置之后界面会显示城市名称 * * @param code * 城市代码 */ public void setCityCode(String code) { this.code = code; if (dbm == null) { return; } Cursor cursor = dbm.queryProvinceAndCity(code); if (cursor != null && cursor.moveToFirst()) { this.province = cursor.getString(cursor.getColumnIndex(PubDBM.CCC_PROVINCE)); this.city = cursor.getString(cursor.getColumnIndex(PubDBM.CCC_CITY)); cursor.close(); } setTextSummary(province, city); } private void setTextSummary(String province, String city) { if (province.equals(city)) { setText(city); } else { setText(province + " " + city); } } @Override public void onClick(View v) { dialog.show(); } // 内部类 class CitySelectDialog extends AlertDialog implements OnItemClickListener,OnClickListener { private Context context; private ListView lvProvince, lvCity; private Cursor pCursor, cCursor; private Button btClear; private SimpleCursorAdapter padapter, cadapter; public CitySelectDialog(Context context) { super(context); this.context = context; } public CitySelectDialog(Context context, int theme) { super(context, theme); this.context = context; } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // this.setView(R.layout.dialog_city_select);// 可能会有边框 this.setContentView(R.layout.dialog_city_select); lvProvince = (ListView) findViewById(R.id.lvProvince); lvCity = (ListView) findViewById(R.id.lvCity); btClear = (Button) findViewById(R.id.btClear); btClear.setOnClickListener(this); if (dbm == null) { return; } pCursor = dbm.queryProvinceList(); pCursor.moveToFirst(); if (province == null) { province = pCursor.getString(pCursor.getColumnIndex(PubDBM.CCC_PROVINCE)); } padapter = new SimpleCursorAdapter(context, R.layout.listview_item_city, pCursor, new String[] { PubDBM.CCC_PROVINCE }, new int[] { R.id.tvSummary }, SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); lvProvince.setAdapter(padapter); lvProvince.setOnItemClickListener(new OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { TextView tvSummary = (TextView) view.findViewById(R.id.tvSummary); tvSummary.setBackgroundResource(android.R.color.holo_blue_light); province = tvSummary.getText().toString(); cadapter.changeCursor(dbm.queryCityList(province)); } }); lvProvince.setOnScrollListener(new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // System.out.println("onScrollStateChanged"+scrollState); } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // System.out.println("onScroll:" + firstVisibleItem + "|" + visibleItemCount + "|" + totalItemCount); for (int i = 0; i < visibleItemCount; i++) { TextView tvSummary = (TextView) view.getChildAt(i).findViewById(R.id.tvSummary); // System.out.println("summary:" + tvSummary.getText().toString() + "||" + province); if (province.equals(tvSummary.getText().toString())) { tvSummary.setBackgroundResource(android.R.color.holo_blue_light); } else { tvSummary.setBackgroundColor(Color.TRANSPARENT); } } } }); cCursor = dbm.queryCityList(province); cCursor.moveToFirst(); code = cCursor.getString(cCursor.getColumnIndex(PubDBM.CCC_CODE)); cadapter = new SimpleCursorAdapter(context, R.layout.listview_item_city, cCursor, new String[] { PubDBM.CCC_CITY, PubDBM.CCC_CODE }, new int[] { R.id.tvSummary, R.id.tvCode }, SimpleCursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER); lvCity.setAdapter(cadapter); lvCity.setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { TextView tvSummary = (TextView) view.findViewById(R.id.tvSummary); TextView tvCode = (TextView) view.findViewById(R.id.tvCode); city = tvSummary.getText().toString(); code = tvCode.getText().toString(); setTextSummary(province, city); cancel(); } @Override public void onClick(View v) { province = ""; city = ""; code = ""; setText(""); cancel(); } } }
代码还是比较简单的,由于时间仓促,二级目录内容的改变直接使用了
cadapter.changeCursor(dbm.queryCityList(province));这个地方可能有更好的方法,不过城市的选择不会是经常选择的东西,应该问题不大。另外还有几个布局文件,都是一个界面上的东西了:
dialog_dity_select.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="360dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" android:background="@android:color/holo_blue_dark" android:orientation="vertical" > <TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_marginTop="6dp" android:layout_marginBottom="8dp" android:gravity="center" android:text="城市选择" android:textColor="@android:color/holo_blue_bright" android:textSize="24sp" /> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent" android:layout_above="@+id/btClear" android:layout_below="@+id/tvTitle" android:padding="10dp" > <ListView android:id="@+id/lvProvince" android:layout_width="121dp" android:layout_height="246dp" android:layout_marginLeft="8dp" android:background="@android:color/holo_blue_bright" android:layout_alignParentLeft="true" android:divider="@null"/> <ListView android:id="@+id/lvCity" android:layout_width="121dp" android:layout_height="246dp" android:layout_marginRight="8dp" android:background="@android:color/holo_blue_bright" android:layout_alignParentRight="true" android:divider="@null"/> </RelativeLayout> <Button android:id="@+id/btClear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_gravity="center_horizontal" android:layout_marginBottom="8dp" android:text="清除选择" android:textColor="@android:color/holo_blue_bright" /> </RelativeLayout>4、使用
这个就比较简单了,直接在xml中写上这个自定义的EditText,如下:
<com.ttdevs.cityselect.util.CitySelect android:id="@+id/etCitySelect" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="请选择城市" />
Activity中:
public class MainActivity extends Activity implements OnClickListener { private CitySelect csCity; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); csCity = (CitySelect) findViewById(R.id.etCitySelect); csCity.setCityCode("101060406"); // 吉林 四平 } @Override public void onClick(View v) { Toast.makeText(getApplicationContext(), "你选择了:"+csCity.getCityCode(), Toast.LENGTH_LONG).show(); } }
5、总结
上图中效果是项目中的,里面图片资源不好拿出来共享,最终的效果如下图。
源码:下载
要想保证信息的传输,目前在smack/asmack + openfire架构上,我个人想到有两种实现方式:
1.端到端确保发送(类似短信)。
其实这个就是xmpp协议的XEP-0184: Message Delivery Receipts.
里边为了确保消息的到达,需要接收方返回回执,这样发送方就知道对方是否确切收到消息。
当然咯,接收回执过程中也可能出现断线,导致发送方收不到回执,而认为接收方没收到,再重新发的问题,这个需要接收方过滤掉重复的信息来解决。
其实这个协议,asmack在0.8.3版本就已经支持,具体什么版本开始,就懒得去研究了。
需要注意的是:这个是两个客户端之间的事情,即openfire什么也不用干,只要客户端都支持xep-0184就可以了。
下面简单说下xep-0184协议的交互内容:
发送方发送一个需要回执的消息:
<message from='northumberland@shakespeare.lit/westminster' id='richard2-4.1.247' to='kingrichard@royalty.england.lit/throne'> <body>My lord, dispatch; read o'er these articles.</body> <request xmlns='urn:xmpp:receipts'/> </message>
接收方收到消息后,返回的消息:
<message from='kingrichard@royalty.england.lit/throne' id='bi29sg183b4v' to='northumberland@shakespeare.lit/westminster'> <received xmlns='urn:xmpp:receipts' id='richard2-4.1.247'/> </message>
注意回执的id要与接收的packetID对应。
好了,了解原理了。大家来看看asmack是怎么实现的。
http://bamboo.igniterealtime.org/browse/SMACK-TRUNK-59/commit 上说的好简单,实际上你不会成功的,因为asmack有bug,目前最新的0.8.5上也没解决。
下面跟大家介绍怎么使用消息回执及解决这个bug。
发送需要回执的消息前,调用
DeliveryReceiptManager.addDeliveryReceiptRequest(packet); myConnection.sendPacket(packet);
来为你的packet添加<request xmlns='urn:xmpp:receipts'/>节点。
在初始化xmppconnection后,调用
DeliveryReceiptManager.getInstanceFor(myConnection) .enableAutoReceipts();
来设置自动进行回执,设置后,回执的事情就不用我们自己操心啦。
好了,要做的事情就这么点。额,,,本来应该就这么点。但是。。asmack有bug啊,他把request跟received都用同一个ExtensionProvider啦!!
证据在org.jivesoftware.smackx.ConfigureProviderManager类里边这两句:
// XEP-184 Message Delivery Receipts pm.addExtensionProvider("received", "urn:xmpp:receipts", new DeliveryReceipt.Provider()); pm.addExtensionProvider("request", "urn:xmpp:receipts", new DeliveryReceipt.Provider());
都用了同一个Provider,当然出问题了。就是接受者无法找到<request xmlns='urn:xmpp:receipts'/>节点,因为DeliveryReceipt.Provider()生成的是received节点。
于是乎,自动回执没有效果。
“改吧!。。”
“不改jar包行不行啊?”
“行啊!”
如下:
sAndroid = SmackAndroid.init(cxtContext); ProviderManager pm = ProviderManager.getInstance(); // add delivery receipts pm.addExtensionProvider(DeliveryReceipt.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceipt.Provider()); pm.addExtensionProvider(DeliveryReceiptRequest.ELEMENT, DeliveryReceipt.NAMESPACE, new DeliveryReceiptRequest.Provider());
将jar包中错误的设置,重新设置一下。这样,消息回执功能就大功告成啦。。
xep-0184里边还讲了如何判断客户端是否支持消息回执的问题,如果你有这样的需求,就自己去了解吧。
2.openfire服务端确保消息发送到达(待续)
我不知是否是由于MINA太旧的原因,导致openfire在nio发送的过程中,无法捕获发送异常,导致无法识别异常断线的客户端,也就不能准确的保存离线消息。
openfire目前用的MINA是1.1.7版本,可能是这个版本有问题。apache在这版本上进行了重大的调整和改造,版本号直接改成2.0.0。因此直接升级openfire的MINA包难度很大,连openfire开发者们都拖了这么些年不更新MINA版本。。
这个原因纯属个人猜测,于是这个方法走不通。有兴趣的同学可以试试,熟悉MINA的人应该能看出来。
既然底层机制不可变,那就只能通过自己的手法来处理了。
这个我目前在实践中,或许下一篇就会讲到。