J2ME中文教程 7 开发无线网络应用程序
作者:whycloud 文章来源:http://www.j2medev.com/Article/ShowArticle.asp?ArticleID=272
7.1 无线网络前景与MIDP联网技术简介7.1.1 无线网络前景有很多人称现在是一个瞬息万变的时代,也有人说当今是一个科技、信息爆炸的时代,那么我回过头来看看是什么使得当今社会有着如此之快的变化。这时你不难发现网络的普及是其中一个十分重要的因素。从邮政网络到电报网络,再到电话网络,再到互联网络,似乎我们的生活已经被各种各样的信息网络所覆盖。那么我们看过网络普及的历史,再来看看现在及展望一下下一个会影响我们生活的是什么网络,在中国无线电话的普及速度超过了我们的想象,那么这个无线的网络会不会成为我们生活中不可获缺的东西呢?我不敢断言,但至少说无线移动网络和互联网的结合会带来不可估量的影响力。
了解了无线网络的美好前景?让我们再来看看它的不足。首先无线网络在当今的技术下与有线网络相比它的带宽更小、延迟更大、连接的稳定性更差。这要求我们在开发无线联网应用程序时,和以往有很大不同。下面让我们来了解一下MIDP2.0在这方面的规定。
7.1.2 J2ME联网技术简介在MIDP2.0规范中规定了两类无线数据网:线路交换数据网(CSD)和包交换数据网。
在线路交换数据网中的,每个用户都有自己的通话频道,在用户进行数据交换的时候该频道不可以被用做他用,记费的方式也是通过时间收费的。这种网络还有一个特点就是传输速率慢,仅为9.6kbps。
另一种包交换数据网,是现在普遍使用的网络。在传输中,数据是被分成比较小的等长的数据包进行交换。不同用户的数据包交换的时候,会被分配到一个通讯频道的不同时间片上,传输过程中不同用户的数据是混合传输的,到达后有接收方重新组装。我们所熟知的GPRS、3G、WCDMA都是包交换传输技术,而其中的3G理论上可以到到几mbps的传输速率。
具体反应在API这个层次J2ME没有沿用J2SE的联网技术,而是使用了一套通用连接框架(Generic Connection Framework,GCF)。实际上GCF是由CLDC定义的,并被MIDP2.0继承下来。这组APIs在javax.microedion.io包中实现。为了适应前面提到的网络条件不好、设备能力不同的问题,MIDP2.0充分的利用了现有网络的基础结构,定义了灵活的策略。在MIDP规范中规定,所有的设备都要支持HTTP和HTTPS协议,并且定义了一些可选协议的API接口:数据报、套接字(socket)、安全套接字(ssl),和串行通信接口。
由于无线网络的各个特点,因此在开发MIDP联网应用程序的时候,要注意的很重要的一点就是许多关于网络通讯的API都是要花费大量CPU计算机时间并且有可能阻塞线程的。那么对于开发人员来说,设计程序的时候,就需要避免在用户界面动作的响应方法中直接调用网络动作。例如CommandLlistener中,不应该含有网络动作的调用,而在其中真正要做的应该是唤醒包含网络动作的线程,这样一来就不会阻塞用户响应线程。同时出于人性化设计的考虑,在对网络操作的时候还该有cancel的操作。这样用户可以在等待时间过长的时候放弃和终止该操作。也正是因为J2ME在无线网络方面大量的线程操作,使得我们可以一边在前台响应用户,一边在后台进行数据交换
下面我们先从GCF入手,然后再逐步地深入和各种连接。
7.2 通用连接框架Generic Connection Framework通用联网框架在J2ME平台中扮演着十分重要的角色。由于移动信息设备的资源受限特性,所以java.net不适合在这里使用。Generic Connection Framework(以下简称GCF)是在CLDC中定义的。它的引入成功的解决了联网的复杂类型。
我们分析GCF的时候可以清楚地发现它有如下几个特性:基于接口设计,便于扩展、提供创建连接的工厂方法、使用标准URL简化了程序员的工作。当我们察看CLDC1.1的api的时候我们可以发现其中定义了8个接口、一个Connector类和一个ConnectionNotFoundException异常。GCF在MIDP2.0中进行了扩展,提供了HttpConnection、HttpsConnection接口,这样使得MIDlet具备了通过Http或者Https协议与server通信的能力;可选的提供了SocketConnection、ServerSocketConnection、UDPDatagramConnection接口,使得MIDlet能够在TCP/IP层通过socket进行通信或者使用数据报进行通信。
GCF的设计从某种意义上讲,比J2SE的网络API体系要清晰的多。
7.2.1 GCF的层次结构让我们结合GCF的接口层次图来了解通用联网框架:

最上层的接口是Connection接口,其他的接口都从它那里继承。在Connection中只定义了一个方法close()。
在我们的现实世界中通常使用的是分组数据交换和电路交换,因此在联网框架中相应的定义了DatagramConnection和StreamConnection。
由于在基于流传输中我们需要对输入流和输出流通是具有操作的能力,因此StreamConnection扩展了InputConnection和OutputConneciton,我们经常使用的Conn.openInputStream(),conn.openOutputStream()方法都是在这两个重要的接口中定义的。
StreamConnectionNotifier接口定义了连接监听器应该具备的能力,它的方法acceptAndOpen()方法返回一个StreamConnection类型的连接,ServerSocketConnection继承了StreamConnectionNotifier接口,这样如果你将你得设备做为socket server的时候就可以通过使用这样的URL:socket://:port在你的设备上建立监听端口等待连接。
SocketConnection继承了StreamConnection正好可以和ServerSocketConnection交相辉映。
UDPDatagramConnection则是为了在分组数据交换中使用,他继承了DatagramConnection接口。
ContentConnection接口中只定义了三个方法getEncoding(),getLength()和getType(),我们非常熟悉的HttpConnection就是他的子类,在HttpConnection中定义了大量的操作,Http联网功能也是MIDP规范中要求厂商必须支持的连接方式。现在你应该对层次比较清楚了吧,继续往下看如何使用GCF。
7.2.2 GCF的使用GCF的使用非常简单,主要集中在Connector的open()方法上。我们要做的就是提供一个标准的URL参数传递给open方法,例如为了得到一个HttpConnection我们应该写类似下面的代码:
String url = “http://myip:myport/myservlet”;
HttpConnection httpConn = (HttpConnection)Connector.open(url);
我们应该清楚这个URL的格式如何定义的,有兴趣的话你可以参考RFC2396,我这里只列入他的基本格式:
{scheme}:[{target}][{parms}]
针对不同的网络通信方式,你要做的就是写出不同的URL,并通过强制转换得到你需要的连接类型。
7.3 熟练掌握Http连接前面我们已经提到过,MIDP规范中规定设备必须支持HTTP协议和HTTPS协议,所以,我们主要也将围绕HttpConnection进行讨论。
7.3.1 HTTP简介首先简单的介绍一下HTTP协议。
HTTP是无状态协议。请求消息被立即发送,理想的情况是没有延时的进行处理的。然而延时是客观存在的。由于HTTP是无状态的,因此其请求和响应的消息如果没有发送并传送成功,那么不保存任何已传递的信息。
HTTP的工作机制是请求和响应,最简单的情况,一个用户输入了一个网站的地址,其实质就是发送了一个请求,然后浏览器返回所请求的页面(响应)。
在HTTP请求中有多种形式,在这里我们只简单的介绍:GET、POST、HEAD三种。
GET:
GET请求返回以URL形式表示的资源,当用户输入一个简单的URL时,就是使用GET请求。Web设计中,GET请求也可以运送query string,不过是依靠在URL后面添加字符串完成的。例如:
http;//localhost;8080/requestdump?quest=sdf
HEAD:
除了服务器禁止在响应中发送消息体外,HEAD和GET完全一样。HEAD请求HTTP头所包含的信息与GET请求的响应中的信息相同,HEAD请求还可以获取请求暗指的消息的有关元信息。
POST:
POST请求将表单体作为一个整体发送。实际上POST请求的功能是由服务器决定的,而且通常依赖于Request-URI相连接的应用程序GET、POST提交表单的不同之处是GET显示了表单的信息,而POST连同请求消息体一起发送表单。
在上面我们介绍了3种比较重要的请求,在后面我们会经常用到。
那么现在让我们回到MIDP上来,首先要说明的是如果你很细心的话,你会发现在javax.microedition.io中有一个很重要的接口javax.eicroedition.io.ContentConnection。我们后面介绍的许多东西都扩展了该接口。例如javax.microedition.io.HttpConnection就是扩展了上述的接口提供了方法以对URL进行分析、设置请求头以及对响应头进行分析。另外值得一提的是,该接口自从MIDP1.0以来没有变化过。
7.3.2 HTTP连接状态一个HTTP连接有三种状态:setup、connected、closed。
一个HTTP被打开,且在发送请求之前,所处的状态就是setup。在这个状态下,应用程序需要设置与服务器进行连接的各种信息,用setRequestMethod和setRequestProperty两个方法进行设置;用getRequestMethod和getRequestProperty方法取得参数当前值。当连接关闭的时候,该连接也就进入了closed状态,不过在这里要强调一点就是,在closed状态下,HttpConnection对象的方法都会变的不可用,不过这里非常强调的就是它打开的InputStream和OutputStream却可能仍然包含数据。
例如:
HttpConnection c=(HttpConnection)Connector.open(URL);
InputStream is=c.openInputStream();
c.close();
while((int ch=is.read())!=-1){
……
}
is.close();
换句话说连接被关闭了,但依然可以读到缓冲区的数据。但不推荐你这么做,最好是处理完所有的工作后,最后关闭连接。
7.3.3 建立HTTP连接
应用程序通过javax.microedition.io.Connector.open这个方法打开连接(在这里要提到的是,这个方法可以打开的不仅仅是HTTP连接,后面所要说的Socket,UDP连接都要通过这个方法打开的)该方法有3个版本,分辨是一个参数、两个参数、和三个参数的。而且该方法是静态的。
javax.microedition.io.Connector.open(String name)
javax.microedition.io.Connector.open(String name,int mode)
javax.microedition.io.Connector.open(String name int mode, boolean timeout)
这个三个参数分别是连接的字符串、读写的类型(javax.microedition.io.Connector.READ、javax.microedition.io.Connector.WRITE、javax.microedition.io.Connector.READ_WRITE)和超时的时候是否抛出异常。
一般情况我们只用到第一个就好了。其中的name 这个参数必须以URI形式提供。比如http://www.j2medev.com。打开一个连接的时候,在服务器响应之前都可能阻塞应用线程。在开发应用程序的时候,一定要确保把打开连接的代码放到一个单独线程中。
如下建立连接的一个例子:
HttpConnection c= (HttpConnection) Connector.open("http://www.j2medev.com");
得到HttpConnection对象后,建立的连接处于setup的状态。
7.3.4 设置HTTP请求头请求头的类型
HTTP协议提供了许多的头标类型,使MIDlet设备和HTTP服务器就发送和接收内容上的一些问题进行协商:
l Connection:如果Connection的值为close,一旦服务器发送应答消息就会关闭连接。不用Connection做为请求头,一个连接可以交换多条消息。
l Connection-Length:包含消息的字节数,但不包含头的字节数。
l Content-Type:描述了消息体中的数据的编码方式。通常头中指定了标题的数据类型,有时字符编码信息也包括其中。该标题最常用的值有:text/html或有时也可能看到下面的情况:
Content-Type:text/html;charset=ISO-8859-1
这说明消息中有一个带有字符的HTTP页面,字符来自ISO-8859-1字符集。使用MIME类型描述数据内容,MIME类型由Internet Assigned Numbers注册。
l Date:指定了消息发送的日期和时间。日期的格式在RFC 2616中有描述,例如:
Data:Sun,23 May 2005 23:57:56 GMT。
l Last-Modified:指明服务器返回资源最后次被修改的日期和时间。其中日期的格式与Date头一样。用户对其加以存储以高速缓存返回的数据。给定此时间,下次请求此数据时缓存 数据的用户通常都带有一个If-Modified-Since头,如果有这个头,则服务器只在数 据的副本更新的情况下才返回。
l Location:Web服务器用它将用户重定向到另一个位置,在这个新的位置可以找到请求的资源。该头的值是一个绝对URL: Location: http://www.host.com/elsewhere.html
l Server:包含有关文本以标识响应的服务器。该头只有以下的信息:
Server: Apache/1.3.14(Unix) PHP/4.0.4
l User-Agent:该请求头标的作用是为服务器标识当前用户的,在MIDP2.0规范中并没有要求MIDlet一定要设置User-Agent属性。下面的片段代码演示了设置User-Agent域的版本号为该设备上CLDC和MIDP实现的版本号的方法
StringBuffer sb=new StringBuffer(60;
sb.append(“Configuration/”);
sb.append(System.getProperty(“microedition.configuration”));
String prof=System.getProperty(“microedition.profiles”);
int i=0;int j=0;
while ((j=prof.indexof(‘’,i))!=-1){
sb.append(“ Profile/”);
sb.append(prof.substring(i,j));
i=j+i;
}
sb.append(“ Profile/”);
sb.append(prof.substring(i));
c.setRequestProperty(“User-agent”,sb.toString());
l Accept-Language:用来设置使用指定的语言请求文档。该请求头的属性一般可以设置为系统属性microedition.locale,还是来看一下代码片段:
String locale=System.getProperty(“microedition.locale”);
if(locale!=null){
c.setRequestProperty(“Accept-Language”,locale);
}
l Authorization:该头标包含了客户端所发送的认证信息,用来访问服务器上受保护的资源,例如在Authorization属性中设置用户ID和密码,服务器只处理已经通过认证的可户端访问请求。在使用基本认证模式的时候,Authorization头标必须包含字符串“Basic”,然后是一个空格,再后是base64编码的用户ID和密码,在用户名和密码之间用“:”分隔。例如,一个客户端要使用用户名book和密码bkpasswd来访问受保护的资源
Authorization; Basic Ym9vazpia3Bhc3N3ZA==
处理请求头
连接处于setup的状态下可以用下面的方法来设置请求的头标
setRequestProperty:设置请求头标的名称和值
setRequestMethod:设置请求的方法为这几种:POST、GET、HEAD(默认的情况下是GET)
getRequestMethod:返回当前请求的方法
getRequestProperty:返回上一次设置的请求属性
请看他们的使用:
c = (HttpConnection)Connector.open(url);
// Set the request method and headers
c.setRequestMethod(HttpConnection.POST);
c.setRequestProperty("If-Modified-Since", "29 Oct 1999 19:43:31 GMT");
c.setRequestProperty("User-Agent","Profile/MIDP-2.0 Configuration/CLDC-1.0");
c.setRequestProperty("Content-Language", "en-US");
7.3.5 使用HTTP连接在MIDlet设置了我们需要的请求头标后,我们就可以使用此连接了,而连接的动作是根据setRequestMethod方法的设置值。如果服务器响应正确的话,我们还可以打开输入流,读入数据。
GET
该请求方法是默认方法,所以如果你不使用setRequestMethod来设置的话,就是说你使用GET方法,下面我们给出一个代码片段来演示GET方法的具体使用。
HttpConnection c;
InputStream is;
Try{
C=(HttpConnection)Connector.open(“http://java.sun.com/”);
//判断连接是否正常
int status=c.getResponseCode();
if(status!=HttpConnection.HTTP_OK)
throw new IOException(“Response code not ok”);
else{
is=c.openInputStream();//打开输入流,读入数据
int ch;
while((ch=is.read())!=-1){
//处理数据
}
}
}finally{
if(is!=null)
is.close();
if(c!=null)
c.close();
}
POST
POST请求比GET请求可以发送更多的参数到服务器端,使用POST请求的时候可以在URL的一部分中添加参数(同GET),同时也可以使用一个流对象把参数传递给服务器。
下面的代码片段演示了使用流传递参数
HttpConnection c;
InputStream is;
OutputStream os;
Try{
c=(HttpConnection)Connector.open(url);
//设置请求为POST
c. setRequestMethod(HttpConnection.POST);
os=c.openOutputStream();
os.write(“some string”.getBytes());
os.flush();
status=c.getResponseCode();
if(status!=HttpConnection.HTTP_OK)
throw new IOException(“Response status not ok”);
}
HEAD
HEAD请求和GET请求类似。不同是服务器不会返回一个消息体,通常使用HEAD请求都是用来测试URL的合法性或是否被修改过
7.3.6 关闭HTTP连接在前面的代码中,其实已经用过javax.microedition.io.Connector.close方法了,也就是使用该方法关闭HTTP连接。下面再把这样一段代码片段单独拿出来演示如何关闭一个HTTP连接
try{
c.close();
}catch(IOException e){
……
}
7.3.7 HTTP示例我们演示《J2ME实现简单电子邮件发送功能》,该文章选自J2ME开发网,原代码属于mingjava。
首先我们构造一个Message类来代表发送的消息。它包括主题、收件人和内容三个字段。
package com.j2medev.mail;
public class Message
{
private String to;
private String subject;
private String content;
public Message()
{
}
public Message(String to, String subject, String content)
{
this.to = to;
this.subject = subject;
this.content = content;
}
public String getContent()
{
return content;
}
public void setContent(String content)
{
this.content = content;
}
public String getSubject()
{
return subject;
}
public void setSubject(String subject)
{
this.subject = subject;
}
public String getTo()
{
return to;
}
public void setTo(String to)
{
this.to = to;
}
public String toString()
{
return to+subject+content;
}
}
在用户界面的设计上,我们需要两个界面。一个让用户输入收件人和主题,另一个用于收集用户输入的内容。由于TextBox要独占一个屏幕的,因此我们不能把他们放在一起。
package com.j2medev.mail;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.Item;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.MIDlet;
public class MainForm extends Form implements CommandListener
{
private MailClient midlet;
private TextField toField;
private TextField subField;
private boolean first = true;
public static final Command nextCommand = new Command("NEXT", Command.OK, 1);
public MainForm(MailClient midlet, String arg0)
{
super(arg0);
this.midlet = midlet;
if(first)
{
first = false;
init();
}
}
public void init()
{
toField = new TextField("To:", null, 25, TextField.ANY);
subField = new TextField("Subject:", null, 30, TextField.ANY);
this.append(toField);
this.append(subField);
this.addCommand(nextCommand);
this.setCommandListener(this);
}
public void commandAction(Command cmd,Displayable disp)
{
if(cmd == nextCommand)
{
String to = toField.getString();
String subject = subField.getString();
if(to == "" && subject == "")
{
midlet.displayAlert("Null to or sub",AlertType.WARNING,this);
}
else
{
midlet.getMessage().setTo(to);
midlet.getMessage().setSubject(subject);
midlet.getDisplay().setCurrent(midlet.getContentForm());
}
}
}
}
package com.j2medev.mail;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.TextBox;
import javax.microedition.midlet.MIDlet;
public class ContentForm extends TextBox implements CommandListener
{
private MailClient midlet;
private boolean first = true;
public static final Command sendCommand = new Command("SEND", Command.ITEM,
1);
public ContentForm(String arg0, String arg1, int arg2, int arg3,
MailClient midlet)
{
super(arg0, arg1, arg2, arg3);
this.midlet = midlet;
if (first)
{
first = false;
init();
}
}
public void init()
{
this.addCommand(sendCommand);
this.setCommandListener(this);
}
public void commandAction(Command cmd, Displayable disp)
{
if (cmd == sendCommand)
{
String content = this.getString();
midlet.getMessage().setContent(content);
System.out.println(midlet.getMessage());
try
{
synchronized (midlet)
{
midlet.notify();
}
} catch (Exception e)
{
}
}
}
}
最后我们完成MIDlet,在其中包括联网的程序代码这是重点。
package com.j2medev.mail;
import java.io.DataOutputStream;
import java.io.IOException;
import javax.microedition.midlet.MIDlet;
import javax.microedition.midlet.MIDletStateChangeException;
import javax.microedition.lcdui.*;
import javax.microedition.io.*;
public class MailClient extends MIDlet
{
private MainForm mainForm;
private ContentForm contentForm;
private Display display;
private Message message;
public Message getMessage()
{
return message;
}
public void setMessage(Message message)
{
this.message = message;
}
public void displayAlert(String text, AlertType type, Displayable disp)
{
Alert alert = new Alert("Application Error");
alert.setString(text);
alert.setType(type);
alert.setTimeout(2000);
display.setCurrent(alert, disp);
}
public ContentForm getContentForm()
{
return contentForm;
}
public Display getDisplay()
{
return display;
}
public MainForm getMainForm()
{
return mainForm;
}
public void initMIDlet()
{
MailThread t = new MailThread(this);
t.start();
message = new Message();
display = Display.getDisplay(this);
mainForm = new MainForm(this, "Simple Mail Client");
contentForm = new ContentForm("Content", null, 150, TextField.ANY, this);
display.setCurrent(mainForm);
}
protected void startApp() throws MIDletStateChangeException
{
initMIDlet();
}
protected void pauseApp()
{
}
protected void destroyApp(boolean arg0) throws MIDletStateChangeException
{
}
}
class MailThread extends Thread
{
private MailClient midlet;
public MailThread(MailClient midlet)
{
this.midlet = midlet;
}
public void run()
{
synchronized(midlet)
{
try
{
midlet.wait();
}
catch(Exception e)
{
e.printStackTrace();
}
}
System.out.println("connecting to server.....");
HttpConnection httpConn = null;
DataOutputStream dos = null;
try
{
httpConn =
(HttpConnection)Connector.open("http://localhost:8088/mail/maildo");
httpConn.setRequestMethod("POST");
dos = new DataOutputStream(httpConn.openOutputStream());
dos.writeUTF(midlet.getMessage().getTo());
dos.writeUTF(midlet.getMessage().getSubject());
dos.writeUTF(midlet.getMessage().getContent());
dos.close();
httpConn.close();
System.out.println("end of sending mail");
}
catch(IOException e)
{}
}
}
在服务器端,我们要完成自己的servlet。他的任务比较简单就是接受客户端的数据然后通过JavaMail发送到指定的收件人那里。这里直接给出servlet代码。
package com.j2medev.servletmail;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.Properties;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.*;
import java.net.*;
public class MailServlet extends HttpServlet
{
private static String host;
private static String from;
public void init(ServletConfig config) throws ServletException
{
super.init(config);
host = config.getInitParameter("host");
from = config.getInitParameter("from");
System.out.println(host + from);
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
doPost(request, response);
}
protected void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
DataInputStream dis = new DataInputStream(request.getInputStream());
String send = dis.readUTF();
String subject = dis.readUTF();
String content = dis.readUTF();
try
{
Properties props = System.getProperties();
// Setup mail server
props.put("mail.smtp.host", host);
// Get session
Session session = Session.getDefaultInstance(props, null);
// Define message
MimeMessage message = new MimeMessage(session);
// Set the from address
message.setFrom(new InternetAddress(from));
// Set the to address
message.addRecipient(Message.RecipientType.TO, new InternetAddress(
send));
// Set the subject
message.setSubject(subject);
// Set the content
message.setText(content);
// Send message
Transport.send(message);
} catch (Exception e)
{
e.printStackTrace();
}
}
}
web.xml如下
<?xml version="1.0" ?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<servlet>
<servlet-name>ServletMail</servlet-name>
<servlet-class>com.j2medev.servletmail.MailServlet</servlet-class>
<init-param>
<param-name>host</param-name>
<param-value>smtp.263.net</param-value>
</init-param>
<init-param>
<param-name>from</param-name>
<param-value>eric.zhan@263.net</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>ServletMail</servlet-name>
<url-pattern>/maildo</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<error-page>
<error-code>404</error-code>
<location>/error.jsp</location>
</error-page>
</web-app>
7.4 Socket连接简介7.4.1 Socket连接简介使用Socket是连接两台计算机最简单的方法,另外由于Socket使用的是TCP协议,所以也就保证了传输的质量。但在这里要注意的是,并不是所有的MIDP设备都支持Socket网络。
在这部分中我们主要涉及到的两个接口是SocketConnection和ServerSocketConnection。这个两个接口的使用方法其实和J2SE中的Socket和ServerSocket类的使用方法很相似。不同的是ServerSocketConnection中打开一个SocketConnection作为监听者的方法是acceptAndOpen()。同时你可以用getLocalAddress()和getLocalPort()两个方法获得本地的绑定IP地址和所打开的端口号,这样你就可以告诉另外一台MIDP设备你所使用的IP和端口,使得另一台MIDP设备可以连接到你的设备上。
在这里我们除了强调使用acceptAndOpen()从一个ServerSocketConnection对象中打开一个SocketConnection作为监听者外,还要说明的是作为套接字我们是可以设置一些属性的,这些属性的设置是通过SocketConnection.setSocketOption()方法来设置。一些属性:
1) 选项
2) 描述
3) DELAY
4) 小缓冲写如延迟值。如果为0,则禁用了TCP对于小缓冲区操作的Nagle算法。如果需要启动该算法则需要把该值设置为非0
5) KEEPLIVE
6) 保持连接的特性。如果该值为0,则禁用了保持连接的特性。如果要启动该特性则要把该值设置为非0
7) LINGER
8) 关闭一个连接前等待未发送的数据发送完毕所经过的秒数。如果该值为0,则禁用了该属性
9) RCVBUF
10) 接受缓冲区的大小,单位字节
11) SNDBUF
12) 发送缓冲区的大小,单位字节
现在让我们来看如何打开一个SocketConnection。
其实打开的方法和打开一个HTTPConnection是一样的,所不同的是,URL给的不同。
见例子:
SocketConnection sc = (SocketConnection)
Connector.open("socket://host.com:79");
sc.setSocketOption(SocketConnection.LINGER, 5);
InputStream is = sc.openInputStream();
OutputStream os = sc.openOutputStream();
os.write("\r\n".getBytes());
int ch = 0;
while(ch != -1) {
ch = is.read();
}
is.close();
os.close();
sc.close();
其实打开一个ServerSocketConnecion方法也是一样的,不同是,不需要给出主机名或IP地址也就是socket://:79这个样子。
同样来看个例子
// Create the server listening socket for port 1234
ServerSocketConnection scn = (ServerSocketConnection)
Connector.open("socket://:1234");
// Wait for a connection.
SocketConnection sc = (SocketConnection) scn.acceptAndOpen();
// Set application specific hints on the socket.
sc.setSocketOption(DELAY, 0);
sc.setSocketOption(LINGER, 0);
sc.setSocketOption(KEEPALIVE, 0);
sc.setSocketOption(RCVBUF, 128);
sc.setSocketOption(SNDBUF, 128);
// Get the input stream of the connection.
DataInputStream is = sc.openDataInputStream();
// Get the output stream of the connection.
DataOutputStream os = sc.openDataOutputStream();
// Read the input data.
String result = is.readUTF();
// Echo the data back to the sender.
os.writeUTF(result);
// Close everything.
is.close();
os.close();
sc.close();
scn.close();
7.4.2 Socket示例其实前面已经说过了,在J2ME中使用Socket和J2SE中差不多,无非就是由服务端打开一个监听端口等待客户端请求该端口,下面我们还来看个具体的示例(该示例代码来自J2ME开发网 mingjava)
package socket;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;
public class SocketMIDlet extends MIDlet implements CommandListener {
private static final String SERVER = "Server";
private static final String CLIENT = "Client";
private static final String[] names = { SERVER, CLIENT };
private static Display display;
private Form f;
private ChoiceGroup cg;
private boolean isPaused;
private Server server;
private Client client;
private Command exitCommand = new Command("Exit", Command.EXIT, 1);
private Command startCommand = new Command("Start", Command.ITEM, 1);
public SocketMIDlet() {
display = Display.getDisplay(this);
f = new Form("Socket Demo");
cg = new ChoiceGroup("Please select peer", Choice.EXCLUSIVE, names,
null);
f.append(cg);
f.addCommand(exitCommand);
f.addCommand(startCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
public boolean isPaused() {
return isPaused;
}
public void startApp() {
isPaused = false;
}
public void pauseApp() {
isPaused = true;
}
public void destroyApp(boolean unconditional) {
if (server != null) {
server.stop();
}
if (client != null) {
client.stop();
}
}
public void commandAction(Command c, Displayable s) {
if (c == exitCommand) {
destroyApp(true);
notifyDestroyed();
} else if (c == startCommand) {
String name = cg.getString(cg.getSelectedIndex());
if (name.equals(SERVER)) {
server = new Server(this);
server.start();
} else {
client = new Client(this);
client.start();
}
}
}
}
package socket;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;
public class Server implements Runnable, CommandListener {
private SocketMIDlet parent;
private Display display;
private Form f;
private StringItem si;
private TextField tf;
private boolean stop;
private Command sendCommand = new Command("Send", Command.ITEM, 1);
private Command exitCommand = new Command("Exit", Command.EXIT, 1);
InputStream is;
OutputStream os;
SocketConnection sc;
ServerSocketConnection scn;
Sender sender;
public Server(SocketMIDlet m) {
parent = m;
display = Display.getDisplay(parent);
f = new Form("Socket Server");
si = new StringItem("Status:", " ");
tf = new TextField("Send:", "", 30, TextField.ANY);
f.append(si);
f.append(tf);
f.addCommand(exitCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
public void start() {
Thread t = new Thread(this);
t.start();
}
public void run() {
try {
si.setText("Waiting for connection");
scn = (ServerSocketConnection) Connector.open("socket://:5009");
// Wait for a connection.
sc = (SocketConnection) scn.acceptAndOpen();
si.setText("Connection accepted");
is = sc.openInputStream();
os = sc.openOutputStream();
sender = new Sender(os);
// Allow sending of messages only after Sender is created
f.addCommand(sendCommand);
while (true) {
StringBuffer sb = new StringBuffer();
int c = 0;
while (((c = is.read()) != '\n') && (c != -1)) {
sb.append((char) c);
}
if (c == -1) {
break;
}
si.setText("Message received - " + sb.toString());
}
stop();
si.setText("Connection is closed");
f.removeCommand(sendCommand);
} catch (IOException ioe) {
if (ioe.getMessage().equals("ServerSocket Open")) {
Alert a = new Alert("Server", "Port 5000 is already taken.",
null, AlertType.ERROR);
a.setTimeout(Alert.FOREVER);
a.setCommandListener(this);
display.setCurrent(a);
} else {
if (!stop) {
ioe.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void commandAction(Command c, Displayable s) {
if (c == sendCommand && !parent.isPaused()) {
sender.send(tf.getString());
}
if ((c == Alert.DISMISS_COMMAND) || (c == exitCommand)) {
parent.notifyDestroyed();
parent.destroyApp(true);
}
}
/**
* Close all open streams
*/
public void stop() {
try {
stop = true;
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
if (sc != null) {
sc.close();
}
if (scn != null) {
scn.close();
}
} catch (IOException ioe) {
}
}
}
package socket;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;
public class Client implements Runnable, CommandListener {
private SocketMIDlet parent;
private Display display;
private Form f;
private StringItem si;
private TextField tf;
private boolean stop;
private Command sendCommand = new Command("Send", Command.ITEM, 1);
private Command exitCommand = new Command("Exit", Command.EXIT, 1);
InputStream is;
OutputStream os;
SocketConnection sc;
Sender sender;
public Client(SocketMIDlet m) {
parent = m;
display = Display.getDisplay(parent);
f = new Form("Socket Client");
si = new StringItem("Status:", " ");
tf = new TextField("Send:", "", 30, TextField.ANY);
f.append(si);
f.append(tf);
f.addCommand(exitCommand);
f.addCommand(sendCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
/**
* Start the client thread
*/
public void start() {
Thread t = new Thread(this);
t.start();
}
public void run() {
try {
sc = (SocketConnection) Connector.open("socket://localhost:5009");
si.setText("Connected to server");
is = sc.openInputStream();
os = sc.openOutputStream();
// Start the thread for sending messages - see Sender's main
// comment for explanation
sender = new Sender(os);
// Loop forever, receiving data
while (true) {
StringBuffer sb = new StringBuffer();
int c = 0;
while (((c = is.read()) != '\n') && (c != -1)) {
sb.append((char) c);
}
if (c == -1) {
break;
}
// Display message to user
si.setText("Message received - " + sb.toString());
}
stop();
si.setText("Connection closed");
f.removeCommand(sendCommand);
} catch (ConnectionNotFoundException cnfe) {
Alert a = new Alert("Client", "Please run Server MIDlet first",
null, AlertType.ERROR);
a.setTimeout(Alert.FOREVER);
a.setCommandListener(this);
display.setCurrent(a);
} catch (IOException ioe) {
if (!stop) {
ioe.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void commandAction(Command c, Displayable s) {
if (c == sendCommand && !parent.isPaused()) {
sender.send(tf.getString());
}
if ((c == Alert.DISMISS_COMMAND) || (c == exitCommand)) {
parent.notifyDestroyed();
parent.destroyApp(true);
}
}
/**
* Close all open streams
*/
public void stop() {
try {
stop = true;
if (sender != null) {
sender.stop();
}
if (is != null) {
is.close();
}
if (os != null) {
os.close();
}
if (sc != null) {
sc.close();
}
} catch (IOException ioe) {
}
}
}
package socket;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import javax.microedition.lcdui.*;
import java.io.*;
public class Sender extends Thread {
private OutputStream os;
private String message;
public Sender(OutputStream os) {
this.os = os;
start();
}
public synchronized void send(String msg) {
message = msg;
notify();
}
public synchronized void run() {
while (true) {
// If no client to deal, wait until one connects
if (message == null) {
try {
wait();
} catch (InterruptedException e) {
}
}
if (message == null) {
break;
}
try {
os.write(message.getBytes());
os.write("\r\n".getBytes());
} catch (IOException ioe) {
ioe.printStackTrace();
}
// Completed client handling, return handler to pool and
// mark for wait
message = null;
}
}
public synchronized void stop() {
message = null;
notify();
}
}
7.5 Datagram连接简介7.5.1 Datagram连接简介提到Datagram网络那么就要对UDP通讯协议做一个简单的介绍了。前面我们介绍的HTTP协议是属于ISO网络曾的应用层,在它下方传输用的是TCP协议,TCP协议在传输数据的时候,如果数据发生错误,那么将重新传输该错误的部分。但是这样以来常常会浪费很多时间,在一些讲究实时性的通讯过程中,这样做有些不切实际。例如我们在观看网络视频的时候,少量的数据丢失并不会有很严重的影响,因此我们就会用到UDP这样的协议。
一个UDP datagram数据包含了地址和数据缓冲区,其中地址是一个URL字符串。在J2ME中发送数据的时候我们使用Datagram.setAddress方法来设置目标地址。(目标地址要包括主机名和端口号)在接收数据的时候,地址是指数据的源地址。数据缓冲区,是一个带有偏移量和长的字节数组,我们的程序可以直接访问该数组,也可以通过DataInputStream和DataOutputStream进行间接的读写。Datagram.getOffset方法对获得数据的偏移量。 通过Datagram.getLength和Datagram.setLength对数据部分的字节长度进行读取和设置。
同样的我们要获得连接就需要用到DatagramConnection,而获得的方法也和前面说到的一样的Connector.open(),所不同的是URL应该满足如下的形式:
l datagram://localhost:5555 这样的话表示建立了一个客户端模式的连接。在指定ip:localhost和指定端口:5555
l datagram://:5555 这样建立的是一个服务器端模式的连接,在本地的5555端口。
建立连接后,我们可以通过DatagramConnection的newDatagram()方法构造一个Datagram,然后调用DatagramConnection的send()方法。
数据流和消息传送:
流套接字从发送方向接受方发送的是连续的数据流,且不要求标记纪录的界限,数据都以不同的包形式发送,而且各个包中的数据也是不同的。
连接和邮寄:
在使用socket连接时需要在发送方和接受方之间建立一个连接,让数据在这个连接中进行传送,这样不需要指明消息的发送方向。但是UDP套接字不需要连接,只是一个单独处理消息的模式,每条消息发往不同的目的地,就好像邮寄东西一样,显然这种发送方式是需要发送方向的。还有一点socket和UDP不同的是,UDP可以接受不同方向的消息,而socket只能接受一个方向的消息。
安全性方面:
在安全性和可靠性方面来说,UDP不如socket来的安全可靠。socket像是一种点对点的连接,中间已经架构了连接,他可以保证发送方的消息发送到接受方(除非断网),万一网络方面有点问题,一旦修复,未发送的消息还是会依次发送,不必担心重发。在这点上UDP做不到,而且在发送过程中有可能出现消息丢失的现象,这就需要用户重发。
7.5.2 Datagram示例说了半天可能有些抽象,还是来看示例(示例来自J2ME开发网 mingjava)
package com.siemens.datagramtest;
import javax.microedition.io.Datagram;
import javax.microedition.io.DatagramConnection;
public class Sender extends Thread
{
private DatagramConnection dc;
private String address;
private String message;
public Sender(DatagramConnection dc)
{
this.dc = dc;
start();
}
public synchronized void send(String addr, String msg)
{
address = addr;
message = msg;
notify();
}
public synchronized void run()
{
while (true)
{
// If no client to deal, wait until one connects
if (message == null)
{
try
{
wait();
} catch (InterruptedException e)
{
}
}
try
{
byte[] bytes = message.getBytes();
Datagram dg = null;
// Are we a sender thread for the client ? If so then there's
// no address parameter
if (address == null)
{
dg = dc.newDatagram(bytes, bytes.length);
} else
{
dg = dc.newDatagram(bytes, bytes.length, address);
System.out.println(address);
}
dc.send(dg);
} catch (Exception ioe)
{
ioe.printStackTrace();
}
// Completed client handling, return handler to pool and
// mark for wait
message = null;
}
}
}
注意联网的时候我们应该在另外一个线程中而不是在主线程中。
服务器端的目的就是启动后监听指定的端口,当客户端连接过来后接受数据并记录下客户端的地址,以便服务器端向客户端发送数据。
package com.siemens.datagramtest;
import java.io.IOException;
import javax.microedition.io.Connector;
import javax.microedition.io.Datagram;
import javax.microedition.io.DatagramConnection;
import javax.microedition.io.UDPDatagramConnection;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;
public class Server implements Runnable, CommandListener
{
private DatagramMIDlet parent;
private Display display;
private Form f;
private StringItem si;
private TextField tf;
private Command sendCommand = new Command("Send", Command.ITEM, 1);
Sender sender;
private String address;
public Server(DatagramMIDlet m)
{
parent = m;
display = Display.getDisplay(parent);
f = new Form("Datagram Server");
si = new StringItem("Status:", " ");
tf = new TextField("Send:", "", 30, TextField.ANY);
f.append(si);
f.append(tf);
f.addCommand(sendCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
public void start()
{
Thread t = new Thread(this);
t.start();
}
public void run()
{
try
{
si.setText("Waiting for connection");
DatagramConnection dc =(DatagramConnection)Connector.open("datagram://:5555");
sender = new Sender(dc);
while (true)
{
Datagram dg = dc.newDatagram(100);
dc.receive(dg);
address = dg.getAddress();
si.setText("Message received - "
+ new String(dg.getData(), 0, dg.getLength()));
}
} catch (IOException ioe)
{
Alert a = new Alert("Server", "Port 5000 is already taken.", null,
AlertType.ERROR);
a.setTimeout(Alert.FOREVER);
a.setCommandListener(this);
display.setCurrent(a);
} catch (Exception e)
{
e.printStackTrace();
}
}
public void commandAction(Command c, Displayable s)
{
if (c == sendCommand && !parent.isPaused())
{
if (address == null)
{
si.setText("No destination address");
} else
{
sender.send(address, tf.getString());
}
}
if (c == Alert.DISMISS_COMMAND)
{
parent.destroyApp(true);
parent.notifyDestroyed();
}
}
public void stop()
{
}
}
客户端代码则是建立连接后向服务器端发送数据,并等待接受服务器返回的数据。
package com.siemens.datagramtest;
import java.io.IOException;
import javax.microedition.io.ConnectionNotFoundException;
import javax.microedition.io.Connector;
import javax.microedition.io.Datagram;
import javax.microedition.io.DatagramConnection;
import javax.microedition.lcdui.Alert;
import javax.microedition.lcdui.AlertType;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.lcdui.StringItem;
import javax.microedition.lcdui.TextField;
public class Client implements Runnable, CommandListener
{
private DatagramMIDlet parent;
private Display display;
private Form f;
private StringItem si;
private TextField tf;
private Command sendCommand = new Command("Send", Command.ITEM, 1);
Sender sender;
public Client(DatagramMIDlet m)
{
parent = m;
display = Display.getDisplay(parent);
f = new Form("Datagram Client");
si = new StringItem("Status:", " ");
tf = new TextField("Send:", "", 30, TextField.ANY);
f.append(si);
f.append(tf);
f.addCommand(sendCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
public void start()
{
Thread t = new Thread(this);
t.start();
}
public void run()
{
try
{
DatagramConnection dc = (DatagramConnection) Connector
.open("datagram://localhost:5555");
si.setText("Connected to server");
sender = new Sender(dc);
while (true)
{
Datagram dg = dc.newDatagram(100);
dc.receive(dg);
// Have we actually received something or is this just a timeout
// ?
if (dg.getLength() > 0)
{
si.setText("Message received - "
+ new String(dg.getData(), 0, dg.getLength()));
}
}
} catch (ConnectionNotFoundException cnfe)
{
Alert a = new Alert("Client", "Please run Server MIDlet first",
null, AlertType.ERROR);
a.setTimeout(Alert.FOREVER);
display.setCurrent(a);
} catch (IOException ioe)
{
ioe.printStackTrace();
}
}
public void commandAction(Command c, Displayable s)
{
if (c == sendCommand && !parent.isPaused())
{
sender.send(null, tf.getString());
}
}
public void stop()
{
}
}
本文的代码取自WTK demo中的例子,您可以参考demo中的源代码!下面给出MIDlet的代码
package com.siemens.datagramtest;
import javax.microedition.lcdui.Choice;
import javax.microedition.lcdui.ChoiceGroup;
import javax.microedition.lcdui.Command;
import javax.microedition.lcdui.CommandListener;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.Form;
import javax.microedition.midlet.MIDlet;
public class DatagramMIDlet extends MIDlet implements CommandListener
{
private static final String SERVER = "Server";
private static final String CLIENT = "Client";
private static final String[] names = { SERVER, CLIENT };
private static Display display;
private Form f;
ChoiceGroup cg;
private boolean isPaused;
private Command exitCommand = new Command("Exit", Command.EXIT, 1);
private Command startCommand = new Command("Start", Command.ITEM, 1);
public DatagramMIDlet()
{
display = Display.getDisplay(this);
f = new Form("Datagram Demo");
cg = new ChoiceGroup("Please select peer", Choice.EXCLUSIVE, names,
null);
f.append(cg);
f.addCommand(exitCommand);
f.addCommand(startCommand);
f.setCommandListener(this);
display.setCurrent(f);
}
public static Display getDisplay()
{
return display;
}
public boolean isPaused()
{
return isPaused;
}
public void startApp()
{
isPaused = false;
}
public void pauseApp()
{
isPaused = true;
}
public void destroyApp(boolean unconditional)
{
}
public void commandAction(Command c, Displayable s)
{
if (c == exitCommand)
{
destroyApp(true);
notifyDestroyed();
} else if (c == startCommand)
{
String name = cg.getString(cg.getSelectedIndex());
if (name.equals(SERVER))
{
Server server = new Server(this);
server.start();
} else
{
Client client = new Client(this);
client.start();
}
}
}
}