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的人应该能看出来。
既然底层机制不可变,那就只能通过自己的手法来处理了。
这个我目前在实践中,或许下一篇就会讲到。