这个例子提供如下:
定义必要的配置sockets的参数的一个简单的用户接口。
service逻辑对于一个见到的echo服务重复这接收到的字节返回给发送者。
模块化原生代码片段来对于Android的原生层方便socket编程。
一个面向连接的socket通信例子。
一个无连接的通信例子。
一个本地的socket通信例子。
建立一个Echo的Android项目。
Abstractt Echo Activity:
为了重复利用者普通的功能,将会创建一个抽象的activity类在定义这时间activity之前。使用Project Explorer 视图,打开src目录,旋转com.apress.echo包,和选择new-》class。设置名字为AbstractEchoActivity和点击这完成按钮。该新文件的内容如下:
package com.apress.echo;
import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
/**
* Abstract echo activity object.
*
* @author Onur Cinar
*/
public abstract class AbstractEchoActivity extends Activity implements
OnClickListener {
/** Port number. */
protected EditText portEdit;
/** Server button. */
protected Button startButton;
/** Log scroll. */
protected ScrollView logScroll;
/** Log view. */
protected TextView logView;
/** Layout ID. */
private final int layoutID;
/**
* Constructor.
*
* @param layoutID
Communication
* layout ID.
*/
public AbstractEchoActivity(int layoutID) {
this.layoutID = layoutID;
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(layoutID);
portEdit = (EditText) findViewById(R.id.port_edit);
startButton = (Button) findViewById(R.id.start_button);
logScroll = (ScrollView) findViewById(R.id.log_scroll);
logView = (TextView) findViewById(R.id.log_view);
startButton.setOnClickListener(this);
}
public void onClick(View view) {
if (view == startButton) {
onStartButtonClicked();
}
}
/**
* On start button clicked.
*/
protected abstract void onStartButtonClicked();
/**
* Gets the port number as an integer.
*
* @return port number or null.
*/
protected Integer getPort() {
Integer port;
try {
port = Integer.valueOf(portEdit.getText().toString());
} catch (NumberFormatException e) {
port = null;
}
return port;
}
/**
* Logs the given message.
*
* @param message
* log message.
*/
protected void logMessage(final String message) {
runOnUiThread(new Runnable() {
public void run() {
logMessageDirect(message);
}
});
}
/**
* Logs given message directly.
*
* @param message
* log message.
*/
protected void logMessageDirect(final String message) {
logView.append(message);
logView.append("\n");
logScroll.fullScroll(View.FOCUS_DOWN);
}
/**
* Abstract async echo task.
*/
protected abstract class AbstractEchoTask extends Thread {
/** Handler object. */
private final Handler handler;
/**
* Constructor.
*/
public AbstractEchoTask() {
handler = new Handler();
}
/**
* On pre execute callback in calling thread.
*/
protected void onPreExecute() {
startButton.setEnabled(false);
logView.setText("");
}
public synchronized void start() {
onPreExecute();
super.start();
}
public void run() {
onBackground();
handler.post(new Runnable() {
public void run() {
onPostExecute();
}
});
}
/**
* On background callback in new thread.
*/
protected abstract void onBackground();
/**
* On post execute callback in calling thread.
*/
protected void onPostExecute() {
startButton.setEnabled(true);
}
}
static {
System.loadLibrary("Echo");
}
AbstractEchoActivity,除了处理houskeeping任务外例如绑定这用户接口组件,提供一个简单的线程实现能够使应用程序来执行这网络操作在单独的线程而不是UI线程。
Echo项目的字符串资源:
<resources>
<string name="app_name">Echo</string>
<string name="title_activity_echo_server">Echo Server</string>
<string name="port_edit">Port Number</string>
<string name="start_server_button">Start Server</string>
<string name="title_activity_echo_client">Echo Client</string>
<string name="ip_edit">IP Address</string>
<string name="start_client_button">Start Client</string>
<string name="send_button">Send</string>
<string name="message_edit">Message</string>
<string name="title_activity_local_echo">Local Echo</string>
<string name="local_port_edit">Port Name</string>
原生的Echo模块:
这原生的echo模块将提供者原生socket接口方法对于这Java应用程序的实现。使用Project Explorer,展开jni目录对于原生源文件,和双击这Echo.cpp C++源文件。代替它的内容如下:
// JNI
#include <jni.h>
// NULL
#include <stdio.h>
// va_list, vsnprintf
#include <stdarg.h>
// errno
#include <errno.h>
// strerror_r, memset
#include <string.h>
// socket, bind, getsockname, listen, accept, recv, send, connect
#include <sys/types.h>
#include <sys/socket.h>
// sockaddr_un
#include <sys/un.h>
// htons, sockaddr_in
#include <netinet/in.h>
// inet_ntop
#include <arpa/inet.h>
// close, unlink
#include <unistd.h>
Download at http://www.pin5i.com/
216 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
// offsetof
#include <stddef.h>
// Max log message length
#define MAX_LOG_MESSAGE_LENGTH 256
// Max data buffer size
#define MAX_BUFFER_SIZE 80
/**
* Logs the given message to the application.
*
* @param env JNIEnv interface.
JNIEnv* env,
jobject obj,
const char* format,
...)
{
// Cached log method ID
static jmethodID methodID = NULL;
// If method ID is not cached
if (NULL == methodID)
{
// Get class from object
jclass clazz = env->GetObjectClass(obj);
// Get the method ID for the given method
methodID = env->GetMethodID(clazz, "logMessage",
"(Ljava/lang/String;)V");
// Release the class reference
env->DeleteLocalRef(clazz);
}
// If method is found
if (NULL != methodID)
{
// Format the log message
char buffer[MAX_LOG_MESSAGE_LENGTH];
va_list ap;
va_start(ap, format);
vsnprintf(buffer, MAX_LOG_MESSAGE_LENGTH, format, ap);
va_end(ap);
Download at http://www.pin5i.com/
217 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
// Convert the buffer to a Java string
jstring message = env->NewStringUTF(buffer);
// If string is properly constructed
if (NULL != message)
{
// Log message
env->CallVoidMethod(obj, methodID, message);
// Release the message reference
env->DeleteLocalRef(message);
}
}
}
/**
* Throws a new exception using the given exception class
* and exception message.
*
* @param env JNIEnv interface.
* @param className class name.
* @param message exception message.
*/
static void ThrowException(
JNIEnv* env,
const char* className,
const char* message)
{
// Get the exception class
jclass clazz = env->FindClass(className);
// If exception class is found
if (NULL != clazz)
{
// Throw exception
env->ThrowNew(clazz, message);
// Release local class reference
env->DeleteLocalRef(clazz);
}
}
/**
* Throws a new exception using the given exception class
* and error message based on the error number.
*
* @param env JNIEnv interface.
* @param className class name.
* @param errnum error number.
*/
static void ThrowErrnoException(
JNIEnv* env,
const char* className,
int errnum)
{
char buffer[MAX_LOG_MESSAGE_LENGTH];
// Get message for the error number
if (-1 == strerror_r(errnum, buffer, MAX_LOG_MESSAGE_LENGTH))
{
strerror_r(errno, buffer, MAX_LOG_MESSAGE_LENGTH);
}
// Throw exception
ThrowException(env, className, buffer);
通过这通信的生命周期,和处理这排序和错误检查括号来自这应用程序。将修改者例子Echo应用程序来包含Tcp服务器和客户端activites为了占建立的连接和信息交换使用sockets。
Echo Server Activity布局
使用Project Explorer视图,展开res目录。展开layout子目录,和创建一个新的布局文件叫做Activity_echo_server.xml.如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
Download at http://www.pin5i.com/
219 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
<EditText
android:id="@+id/port_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/port_edit"
android:inputType="number" >
<requestFocus />
</EditText>
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/start_server_button" />
</LinearLayout>
<ScrollView
android:id="@+id/log_scroll"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<TextView
android:id="@+id/log_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</ScrollView>
</LinearLayout>
这个Echo Server提供了一个简单的用户接口来获得端口来绑定服务器和来呈现着状态更新来着这原生Tcp 服务区在它正在执行。
Echo Server Activity:
使用创建EchoServerAcitivity.java在src目录下。
package com.apress.echo;
/**
* Echo server.
*
* @author Onur Cinar
*/
public class EchoServerActivity extends AbstractEchoActivity {
/**
* Constructor.
*/
public EchoServerActivity() {
super(R.layout.activity_echo_server);
}
protected void onStartButtonClicked() {
Integer port = getPort();
if (port != null) {
ServerTask serverTask = new ServerTask(port);
serverTask.start();
}
}
/**
* Starts the TCP server on the given port.
*
* @param port
* port number.
* @throws Exception
*/
private native void nativeStartTcpServer(int port) throws Exception;
/**
* Starts the UDP server on the given port.
*
* @param port
* port number.
* @throws Exception
*/
private native void nativeStartUdpServer(int port) throws Exception;
/**
* Server task.
*/
private class ServerTask extends AbstractEchoTask {
/** Port number. */
private final int port;
/**
* Constructor.
*
* @param port
* port number.
*/
public ServerTask(int port) {
this.port = port;
}
protected void onBackground() {
logMessage("Starting server.");
try {
nativeStartTcpServer(port);
} catch (Exception e) {
logMessage(e.getMessage());
}
logMessage("Server terminated.");
}
}
}
实现原生TCP服务:
使用Project Explorer,选择这EchoServerActivity,和产生C和C++头文件。打开Echo.cpp源文件。出入include语句,如下:
#include "com_apress_echo_EchoServerActivity.h"
创建一个Socket:socket
一个socket通过一个叫做socket描述符的整形术来代表。Socket API函数,而不是这创建这socket本身,需要一个有效的socket描述符对于这个函数。一个socket函数被创建使用这socket函数。
int socket(int domain,int type,int protocol);
这个socket函数需要如下参数来创建一个新的socket:
Domain指定这socketdomain,通信选择的协议族。在这写的期间,接下来的协议族被Android平台所支持。
PF_LOCAL:Host-interanl通信协议。这个通信协议能够使用应用程序那运行在同样的设备来使用Socket APIs来相互通信。
PF_INET:internet 版本4端口协议。这个端口使应用协议和正在这个网络运行的的应用程序相互通信。
Type指定了通信的语义。接下的主要的socket类型被支持的:
SOCKET_STREAM:流端口类型同基于连接的通信使用Tcp端口。
SOCKET_DGRAM:数据报类型提供了无连接的通信使用UDP端口。
Protocol指定了将要被使用的端口。对于大部分的端口协议族和类型,仅仅只用一种坑能的端口被使用。为了选择默认的端口,这个参数被设置为0.
socket函数返回相关联的socket描述符;-1和错误errno的全局变量被设置到这恰当的error。
NewTcpSocket帮助函数对于Echo.cpp原生模块
* @throws IOException
*/
static int NewTcpSocket(JNIEnv* env, jobject obj)
{
// Construct socket
LogMessage(env, obj, "Constructing a new TCP socket...");
int tcpSocket = socket(PF_INET, SOCK_STREAM, 0);
// Check if socket is properly constructed
if (-1 == tcpSocket)
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
return tcpSocket;
}
这个帮助函数创建了一个新的TCP socket和失败是抛出一个java.lang.IOException异常。
绑定这Socket到一个地址:bind
当一个Socket被创建通过socket函数,它存在一个socket协议族空间没有一个端口地址分配给它。对于客户端能够指定和连接到这个端口,它需要首先绑定一个地址。一个socket能够绑定到一个地址使用bind函数。
int bind(int socketDescriptor,const struct sockaddr* address,socklen_t addressLength);
这绑定函数需要如下参数为了绑定着socket到一个地址:
这socket描述符指定socket实例将绑定到这给定的地址。
这地址指定了端地址,socket将会绑定的。
这地址长度指定了端口地址结构的长度被传递给这个函数。
取决于端口地址,一个不同特色的端口地址被使用。对于PF_INET端口协议,这socketaddr_in结构体被使用来指定和端口地址。这socketaddr_in结构体的定义如下:
struct sockaddr_in {
sa_family_t sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
}
如果这个端口被恰当的绑定,和bind函数将返回0;否则,它将返回-1和这errno全局变量到这恰当错误。
使用这Editor视图,添加这BindSocketToPort帮助函数到Echo.cpp本地模块源文件如下:
/**
* Binds socket to a port number.
*
* @param env JNIEnv interface.
* @param obj object instance.
* @param sd socket descriptor.
* @param port port number or zero for random port.
* @throws IOException
*/
static void BindSocketToPort(
JNIEnv* env,
jobject obj,
int sd,
unsigned short port)
{
struct sockaddr_in address;
// Address to bind socket
memset(&address, 0, sizeof(address));
address.sin_family = PF_INET;
// Bind to all addresses
address.sin_addr.s_addr = htonl(INADDR_ANY);
// Convert port to network byte order
address.sin_port = htons(port);
// Bind socket
LogMessage(env, obj, "Binding to port %hu.", port);
if (-1 == bind(sd, (struct sockaddr*) &address, sizeof(address)))
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
}
如果端口号被设定为0在地址结构体中,这bind函数将分配第一个可能的端口号给这个socket。端口号可以被获得通过使用getsocketname函数。如下:
/**
* Gets the port number socket is currently binded.
*
* @param env JNIEnv interface.
* @param obj object instance.
* @param sd socket descriptor.
* @return port number.
* @throws IOException
*/
static unsigned short GetSocketPort(
JNIEnv* env,
jobject obj,
int sd)
{
unsigned short port = 0;
Download at http://www.pin5i.com/
225 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
struct sockaddr_in address;
socklen_t addressLength = sizeof(address);
// Get the socket address
if (-1 == getsockname(sd,
(struct sockaddr*) &address,
&addressLength))
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
else
{
// Convert port to host byte order
port = ntohs(address.sin_port);
LogMessage(env, obj, "Binded to random port %hu.", port);
}
return port;
}
你可能注意到,这个端口号并没有直接的传递到sockaddr_in结构体。代替的是,这htons函数被用来首先做转换。这是由于host和网络字节的排序的不同。
网络字节的排序:
不同的机器架构使用不同的规则对于数据的排序在硬件水平。下面是著名的字节顺序:
大端排序:先存储大端字节。
小端排序:先存储最后有意义的字节。
带有不同字节排序的规则的机器不能直接交换数据。为了使用带有不同字节排序规则的机器相互通信通过网络,这internet端口定义了大端排序作为官方的网络字节排序转换对于数据的转换。
正如Java虚拟机已经使用了大端字节排序,这第一次你正听说
字节序数据。Java应用程序并没有做任何转换对于数据当在网络上传输数据时。相比而言,对于原生的组件不能被执行在Java虚拟机上,他们使用的机器自己排序如下:
ARM和x86机器架构使用小端字节排序。
MIPS机器架构使用达到字节排序。
当通过网络通信,这原生的代码必须在机器字节排序和网络字节排序镜像转换。
这Socket库提供了一套转换函数来使原生的应用程序透明的操作字节顺序的转换。这些函数被定义通过这sys/endian.h头文件:
#include<sys/endian.h>
这接下转换函数被提供如下:
htons函数转换一个无符号short从host机器字节顺序到网络字节顺序。
ntohs函数和htons相反,通过转换一个无符号的short类型数据从网络到host机器的字节顺序。
htonl函数转换一个无符号的整形术从host机器字节顺序到网络字节顺序。
ntohl函数和htonl函数相反。
监听到来的连接:listen
通过listen函数监听一个socket:
int listen(int socketDescriptor,int backlog);
这个监听函数需要如下的参数被提供来开始监听到来的连接:
这socket描述符指定了socket实例这应用程序想要开始监听到来的连接。
这backlog指定这队列的长度来保存着pending到来的连接。如果应用程序时繁忙的服务一个客户端,其他到来的连接排队到一定数量的pengding有backlog。当backlog是满时,连接被拒绝。
如果函数成功返回0,否则-1;
/**
* Listens on given socket with the given backlog for
* pending connections. When the backlog is full, the
* new connections will be rejected.
*
* @param env JNIEnv interface.
* @param obj object instance.
* @param sd socket descriptor.
* @param backlog backlog size.
* @throws IOException
*/
static void ListenOnSocket(
JNIEnv* env,
jobject obj,
int sd,
int backlog)
{
// Listen on socket with the given backlog
LogMessage(env, obj,
"Listening on socket with a backlog of %d pending connections.",
backlog);
if (-1 == listen(sd, backlog))
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
}
接收到来的连接:accept
这accept函数被明确的用来放置到来的连接从监听队列到接收它。
int accept(int socketDescriptor,struct sockeaddr* address,socklen_t* addressLength);
这个accept函数时一个阻塞函数。如果没有阻塞到来的连接请求在监听队列,它放置着调用线程到一个阻塞的状态知道一个新到来的连接到达。这accept函数需要的参数:
这socket 描述符指定了这socket的实例,应用程序想要接收一个pending的到来的连接。
这地址的长度指针提供了一个地址结构,填充了要连接的客户端的端口地址。如果信息不被应用程序所需要,它被设置为null。
这地址的长度指针同了连接客户端端口地址的长度要分配的内存空间。如果信息不被需要则设置为NULL。
accept请求成功,这函数返回客户端描述符,否则为-1和errno全局变量被设置。
LogAddress帮助函数到原生模块源文件,如下:
/**
* Logs the IP address and the port number from the
* given address.
*
* @param env JNIEnv interface.
* @param obj object instance.
* @param message message text.
* @param address adress instance.
* @throws IOException
*/
static void LogAddress(
JNIEnv* env,
jobject obj,
const char* message,
const struct sockaddr_in* address)
{
char ip[INET_ADDRSTRLEN];
// Convert the IP address to string
if (NULL == inet_ntop(PF_INET,
&(address->sin_addr),
ip,
INET_ADDRSTRLEN))
Download at http://www.pin5i.com/
229 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
else
{
// Convert port to host byte order
unsigned short port = ntohs(address->sin_port);
// Log address
LogMessage(env, obj, "%s %s:%hu.", message, ip, port);
}
}
static int AcceptOnSocket(
JNIEnv* env,
jobject obj,
int sd)
{
struct sockaddr_in address;
socklen_t addressLength = sizeof(address);
// Blocks and waits for an incoming client connection
// and accepts it
LogMessage(env, obj, "Waiting for a client connection...");
int clientSocket = accept(sd,
(struct sockaddr*) &address,
&addressLength);
// If client socket is not valid
if (-1 == clientSocket)
Download at http://www.pin5i.com/
230 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
else
{
// Log address
LogAddress(env, obj, "Client connection from ", &address);
}
return clientSocket;
}
recv函数是一个阻塞函数。如果没有数据从给定的socket被接收到,它将放着调用进程到阻塞状态直到数据可以使用。这recv函数需要的参数如下:
这socket描述符指定了socket实例,应用程序想要接收数据的socket实例。
这buffer指针是将要填充接收到数据的内存地址。
Flages中指定了可惜标记。
如果recv函数成功,它将返回接收到数据的字节数目;否则,它将返回-1和errno全局变量被设置恰当的错误。如果函数返回0,它将指示socket是断开连接的。使用如下:
static ssize_t ReceiveFromSocket(
JNIEnv* env,
jobject obj,
int sd,
char* buffer,
size_t bufferSize)
{
// Block and receive data from the socket into the buffer
LogMessage(env, obj, "Receiving from the socket...");
ssize_t recvSize = recv(sd, buffer, bufferSize - 1, 0);
// If receive is failed
if (-1 == recvSize)
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
else
{
// NULL terminate the buffer to make it a string
buffer[recvSize] = NULL;
// If data is received
if (recvSize > 0)
{
LogMessage(env, obj, "Received %d bytes: %s",
recvSize, buffer);
}
else
{
LogMessage(env, obj, "Client disconnected.");
}
}
return recvSize;
}
发送数据到Socket:send
发送数据到socket通过发送函数:
ssize_t send(int socketDescriptor,void* buffer,size_t bufferLeng,int flags);
像recv函数,这发送函数也是个阻塞函数。如果这socket是繁忙的发送数据,它放置调用进程到一个阻塞状态直到对于传输的数据准备好。这发送函数需要接下的参数被提供为了及时一个pending到来的连接:
这socket描述符指定了应用程序想要发送数据给的socket实例
这buffer指针指向了要发送数据的内存地址。
这buffer长度指定了这buffer的大小。这send函数仅仅传输者buffer到这个长度和返回。
Flages指定额外的发送标记。
send函数返回传输的的字节数据如果成功,否则-1和制定了恰当错误的全局变量errno。像recvSendToSocket帮助函数,如下:
static ssize_t SendToSocket(
JNIEnv* env,
jobject obj,
int sd,
const char* buffer,
size_t bufferSize)
{
// Send data buffer to the socket
LogMessage(env, obj, "Sending to the socket...");
ssize_t sentSize = send(sd, buffer, bufferSize, 0);
Download at http://www.pin5i.com/
233 CHAPTER 8: POSIX Socket API: Connection-Oriented Communication
// If send is failed
if (-1 == sentSize)
{
// Throw an exception with error number
ThrowErrnoException(env, "java/io/IOException", errno);
}
else
{
if (sentSize > 0)
{
LogMessage(env, obj, "Sent %d bytes: %s", sentSize, buffer);
}
else
{
LogMessage(env, obj, "Client disconnected.");
}
}
return sentSize;
}