对于普通应用的网络模块一般使用http文本协议,在android开发中使用http协议比较简单,sdk已经做了很好的封装了,具体使用方法可以参考我的这篇博文。而在游戏开发中,可以结合使用http和socket,当然了http协议底层也是基于tcp协议的。http协议是无连接、无状态的,每次连接只能处理一个请求,然后就断了,而且发一个请求需要附加额外信息(请求行、请求头),每次请求都需要重新建立连接;使用socket的好处是更高效和省流量,建立一次连接后,只要不手动或者出现异常断开,就可以一直互相发送数据,而且是直接以字节的形式发送,不需要额外的附加信息,缺点就是难度加大了,需要服务端和客户端很好的配合,保证发送和读取时数据的顺序一致。本文通过一个简单的demo介绍开发android网游时socket的使用方法,主要包括:android客户端和一个简单的使用java实现的server端,实现客户端和服务端互相发送数据的功能。
1.客户端代码实现
首先创建一个Android应用,名称:AndroidSocketTest
然后分别创建4个文件:BytesUtil.java、BytesReader.java、BytesWriter.java、TCPCommunication.java,下面分别介绍这几个文件的用处和源码:
BytesUtil.java:包含了一些静态工具方法:基本类型和字节数组的相互转换,字符串和字节数组的相互转换,字节数组的赋值和大小重置,对输入流进行读取保存等。比较简单,下面直接看源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
/**
author:alexzhou
email :zhoujiangbohai@163.com
date :2012-11-7
**/
public
final
class
BytesUtil {
/**
*整型转换成字节数组
* @param value 要转换的整型值
* @return
*/
public
static
byte
[] shortToBytes(
int
value) {
byte
[]write =
new
byte
[
2
];
write[
0
] = (
byte
)( (value >>>
8
) &
0xFF
);
write[
1
] = (
byte
)( (value >>>
0
) &
0xFF
);
return
write;
}
public
static
byte
[] intToBytes(
int
value) {
byte
[]write =
new
byte
[
4
];
write[
0
] = (
byte
)( (value >>>
24
) &
0xFF
);
write[
1
] = (
byte
)( (value >>>
16
) &
0xFF
);
write[
2
] = (
byte
)( (value >>>
8
) &
0xFF
);
write[
3
] = (
byte
)( (value >>>
0
) &
0xFF
);
return
write;
}
public
static
byte
[] longToBytes(
long
value) {
byte
[]write =
new
byte
[
8
];
write[
0
] = (
byte
)( (value >>>
56
) &
0xFF
);
write[
1
] = (
byte
)( (value >>>
48
) &
0xFF
);
write[
2
] = (
byte
)( (value >>>
40
) &
0xFF
);
write[
3
] = (
byte
)( (value >>>
32
) &
0xFF
);
write[
4
] = (
byte
)( (value >>>
24
) &
0xFF
);
write[
5
] = (
byte
)( (value >>>
16
) &
0xFF
);
write[
6
] = (
byte
)( (value >>>
8
) &
0xFF
);
write[
7
] = (
byte
)( (value >>>
0
) &
0xFF
);
return
write;
}
/**
* 字节数组转换成整型
* @param value
* @return
*/
public
static
int
bytesToInt(
byte
[]value) {
int
i1 = (value[
0
] &
0xFF
) <<
24
;
int
i2 = (value[
1
] &
0xFF
) <<
16
;
int
i3 = (value[
2
] &
0xFF
) <<
8
;
int
i4 = (value[
3
] &
0xFF
) <<
0
;
return
(i1 | i2 | i3 | i4);
}
public
static
short
bytesToShort(
byte
[] value) {
int
s1 = (value[
0
] &
0xFF
) <<
8
;
int
s2 = (value[
1
] &
0xFF
) <<
0
;
return
(
short
)(s1 | s2);
}
public
static
long
bytesToLong(
byte
[] value) {
long
L1 = (value[
0
] &
0xFF
) <<
56
;
long
L2 = (value[
1
] &
0xFF
) <<
48
;
long
L3 = (value[
2
] &
0xFF
) <<
40
;
long
L4 = (value[
3
] &
0xFF
) <<
32
;
long
L5 = (value[
4
] &
0xFF
) <<
24
;
long
L6 = (value[
5
] &
0xFF
) <<
16
;
long
L7 = (value[
6
] &
0xFF
) <<
8
;
long
L8 = (value[
7
] &
0xFF
) <<
0
;
return
(L1 | L2 | L3 | L4 | L5 | L6 | L7 | L8);
}
/**
* 从指定字节数组中拷贝部分数据
* @param origin
* @param from
* @param to
* @return
*/
public
static
byte
[] copyBytes(
byte
[] origin,
int
from,
int
to) {
int
len = to - from;
if
(len <
0
|| origin.length - from <=
0
) {
throw
new
IllegalArgumentException(
"copyBytes->error arguments:to="
+to+
",from="
+from);
}
byte
[] ret =
new
byte
[len];
if
(len ==
0
)
return
ret;
System.arraycopy(origin, from, ret,
0
, Math.min(len, origin.length - from));
return
ret;
}
/**
* 重置字节数组的大小,然后把原内容复制到新的字节数组中
* @param origin
* @param newSize
* @return
*/
public
static
byte
[] resizeBytes(
byte
[] origin,
int
newSize) {
if
(newSize <
0
) {
throw
new
IllegalArgumentException(
"resizeBytes->newSize must >= 0"
);
}
byte
[] ret =
new
byte
[newSize];
if
(newSize ==
0
)
return
ret;
System.arraycopy(origin,
0
,ret,
0
,Math.min(origin.length, newSize));
return
ret;
}
/**
* 读取输入流中字节,并保存到指定的字节数组中
* @param is
* @param data
* @param off
* @param len
*/
public
static
void
readData(InputStream is,
byte
data[],
int
off,
int
len) {
int
hasRead =
0
;
final
int
BUFFER =
1024
;
while
(hasRead < len) {
try
{
int
remain = len - hasRead;
int
count = is.read(data, off + hasRead, remain > BUFFER ? BUFFER : remain);
if
(count <
0
)
throw
new
IOException(
"readData->read data error"
);
hasRead += count;
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
}
|
BytesReader.java:从服务端接收数据时使用,定义了一个字节数组类型的成员变量,用来保存从输入流中读取的数据。封装了一些从该字节数组中读取相应数据类型的函数,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
/**
* 接受服务端数据时,读取字节并转换到相应类型
author:alexzhou
email :zhoujiangbohai@163.com
date :2012-11-7
**/
public
final
class
BytesReader {
private
final
byte
[]data;
//字节数组的大小
private
final
int
size;
//当前读取的位置
private
int
position;
public
BytesReader(
byte
[]data) {
this
.data = data;
this
.size = data.length;
this
.position =
0
;
}
public
byte
[] read(
int
len) {
if
(len <
0
)
return
null
;
byte
[] value = BytesUtil.copyBytes(data, position, position + len);
this
.position += len;
return
value;
}
public
int
getSize() {
return
size;
}
public
boolean
isAvailable() {
return
size - position >
0
;
}
public
short
readShort() {
byte
[] value = read(
2
);
return
BytesUtil.bytesToShort(value);
}
public
int
readInt() {
byte
[] value = read(
4
);
return
BytesUtil.bytesToInt(value);
}
public
long
readLong() {
byte
[] value = read(
8
);
return
BytesUtil.bytesToLong(value);
}
public
byte
readByte() {
int
value =
this
.isAvailable() ? (
0xFF
& data[position++]) : -
1
;
return
(
byte
)value;
}
public
byte
[] readBytes() {
int
len = readShort();
//读取大数据
if
(len >=
0xFFFF
) {
len =
this
.readInt();
}
return
len ==
0
?
null
: read(len);
}
public
String readUTF() {
byte
[] bytes = readBytes();
if
(
null
!= bytes) {
try
{
return
new
String(bytes,
"UTF-8"
);
}
catch
(UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return
null
;
}
}
|
BytesWriter.java:向服务端发送数据时使用,跟BytesReader.java对应,也是把需要发送的数据保存到字节数组中,然后一次性发送给服务器。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
/**
author:alexzhou
email :zhoujiangbohai@163.com
date :2012-11-9
**/
public
final
class
BytesWriter {
private
byte
[] data;
private
int
count;
public
BytesWriter() {
this
(
64
);
}
public
BytesWriter(
int
size) {
this
.data =
new
byte
[size];
}
public
byte
[] getBytes() {
return
this
.data.length == count ? data : count ==
0
?
null
: BytesUtil.resizeBytes(
this
.data, count);
}
public
void
write(
byte
[] value) {
this
.write(value,
0
, value ==
null
?
0
: value.length);
}
public
void
write(
byte
[] d,
int
offset,
int
len) {
if
(d ==
null
|| len ==
0
)
return
;
int
newCount = count + len;
if
(newCount >
this
.data.length) {
int
newSize = Math.max(
this
.data.length <<
1
, newCount);
this
.data = BytesUtil.resizeBytes(
this
.data, newSize);
}
System.arraycopy(d, offset,
this
.data,
this
.count, len);
this
.count = newCount;
}
public
void
writeInt(
int
value) {
this
.write(BytesUtil.intToBytes(value));
}
public
void
writeShort(
int
value) {
this
.write(BytesUtil.shortToBytes(value));
}
public
void
writeLong(
long
value) {
this
.write(BytesUtil.longToBytes(value));
}
public
void
writeByte(
byte
value) {
int
newCount = count +
1
;
if
(newCount >
this
.data.length) {
int
newSize = Math.max(
this
.data.length <<
1
, newCount);
this
.data = BytesUtil.resizeBytes(
this
.data, newSize);
}
this
.data[count] = value;
this
.count = newCount;
}
public
void
writeBytes(
byte
[] value) {
int
length = (value ==
null
?
0
: value.length);
//发送大数据时
if
(length >=
0xFFFF
) {
this
.writeShort(
0xFFFF
);
this
.writeInt(length);
}
else
{
//告诉服务端发送的数据的大小
this
.writeShort(length);
}
this
.write(value);
}
public
void
writeUTF(String value) {
if
(value ==
null
|| value.length() ==
0
) {
this
.writeShort(
0
);
}
byte
[] bytes =
null
;
try
{
bytes = value.getBytes(
"UTF-8"
);
}
catch
(UnsupportedEncodingException e) {
e.printStackTrace();
}
this
.writeBytes(bytes);
}
}
|
TCPCommunication.java:上面的三个类都是辅助工具类,是给它使用的,它负责socket的创建和连接服务器、读取和发送数据,以及通过Handler发送消息给UI线程更新界面显示数据。由于我们的目的是使用socket通信,所以去掉了很多代码,这里只是简单的建立一次连接,发送和接收完数据后就关闭连接了。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
public
class
TCPCommunication
implements
Runnable {
private
Socket mSocket;
private
DataInputStream mDis;
private
DataOutputStream mDos;
private
Handler mHandler;
private
volatile
boolean
isRunning =
false
;
public
TCPCommunication(Handler handler) {
this
.mHandler = handler;
}
public
boolean
isRunning() {
return
isRunning;
}
public
void
startWork() {
Thread t =
new
Thread(
this
);
t.setPriority(Thread.MAX_PRIORITY);
t.start();
}
public
void
stopWork() {
this
.isRunning =
false
;
}
@Override
public
void
run() {
try
{
mSocket =
new
Socket(
"10.0.2.2"
,
8888
);
mDis =
new
DataInputStream(mSocket.getInputStream());
mDos =
new
DataOutputStream(mSocket.getOutputStream());
//开始发送数据到服务端
BytesWriter bw =
new
BytesWriter();
String username =
"alexzhou"
;
String password =
"123456"
;
int
flag1 =
12345
;
short
flag2 =
12
;
long
flag3 = 100000000L;
bw.writeUTF(username);
bw.writeUTF(password);
bw.writeInt(flag1);
bw.writeShort(flag2);
bw.writeLong(flag3);
byte
[] data = bw.getBytes();
BytesWriter out =
null
;
if
(data.length >=
0xFFFF
) {
out =
new
BytesWriter(data.length +
6
);
out.writeShort(
0xFFFF
);
out.writeInt(data.length);
}
else
{
out =
new
BytesWriter(data.length +
2
);
out.writeShort(data.length);
}
out.write(data);
mDos.write(out.getBytes());
//开始从服务端接收数据
int
len = mDis.readShort();
if
(len ==
0xFFFF
) {
len = mDis.readInt();
}
byte
[] inputData =
new
byte
[len];
BytesUtil.readData(mDis, inputData,
0
, len);
BytesReader br =
new
BytesReader(inputData);
String user_id = br.readUTF();
String recv_username = br.readUTF();
String nickname = br.readUTF();
int
i = br.readInt();
short
s = br.readShort();
long
l = br.readLong();
String result =
"登录成功~您的信息如下\n id:"
+user_id +
"\n用户名:"
+ recv_username +
"\n昵称:"
+ nickname +
"\n 序列号:"
+ i +
"-"
+ s +
"-"
+ l;
Message msg = mHandler.obtainMessage();
msg.what =
0
;
msg.obj = result;
mHandler.sendMessage(msg);
mDis.close();
mDos.close();
mSocket.close();
}
catch
(UnknownHostException e) {
e.printStackTrace();
}
catch
(IOException e) {
e.printStackTrace();
}
};
}
|
MainActivty.java的代码比较简单,源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
public
class
MainActivity
extends
Activity {
private
TextView mTextView;
private
Button mButton;
@Override
public
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)
this
.findViewById(R.id.text);
mButton = (Button)
this
.findViewById(R.id.button_send);
mButton.setOnClickListener(
new
OnClickListener() {
@Override
public
void
onClick(View v) {
new
TCPCommunication(mHandler).startWork();
}
});
}
private
Handler mHandler =
new
Handler() {
@Override
public
void
handleMessage(Message msg) {
String result = (String)msg.obj;
mTextView.setText(result);
}
};
@Override
public
boolean
onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_main, menu);
return
true
;
}
}
|
到此为止,Android客户端的代码已经全部完成了,记得给应用添加访问网络的权限哦:在配置文件中添加
1
|
<
uses-permission
android:name
=
"android.permission.INTERNET"
/>
|
2.服务端代码实现
服务端的代码比较简单,当然了,一般在实际的开发中,服务端和客户端不是同一个人写的,这里我就把客户端里的那几个工具类拿来直接用了。
创建一个java项目,然后把BytesReader.java、BytesWriter.java、BytesUtil.java三个文件拷贝到项目的包下,然后创建Server.java,功能比较简单,监听8888端口,然后等待客户端连接,接收和发送数据。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
/**
author:alexzhou
email :zhoujiangbohai@163.com
date :2012-11-7
**/
public
final
class
Server
implements
Runnable{
private
ServerSocket mServerSocket;
private
Socket mClient;
private
DataInputStream mDis;
private
DataOutputStream mDos;
private
boolean
isRunning =
false
;
public
void
start() {
this
.isRunning =
true
;
Thread t =
new
Thread(
this
);
t.start();
}
public
void
stop() {
this
.isRunning =
false
;
}
public
static
void
main(String []args) {
new
Server().start();
}
@Override
public
void
run() {
try
{
mServerSocket =
new
ServerSocket(
8888
);
System.out.println(
"start server"
);
while
(isRunning) {
System.out.println(
"wait client connect!!"
);
mClient = mServerSocket.accept();
//接收客户端发送的数据
mDis =
new
DataInputStream(mClient.getInputStream());
//数据的长度
int
len = mDis.readShort();
if
(len ==
0xFFFF
) {
len = mDis.readInt();
}
System.out.println(
"client data size:"
+ len);
byte
[] inputData =
new
byte
[len];
BytesUtil.readData(mDis, inputData,
0
, len);
BytesReader br =
new
BytesReader(inputData);
String username = br.readUTF();
String password = br.readUTF();
int
i = br.readInt();
short
s = br.readShort();
long
l = br.readLong();
System.out.println(
"username:"
+username+
";password="
+password+
"Long="
+l);
//向客户端发送数据
mDos =
new
DataOutputStream(mClient.getOutputStream());
BytesWriter bw =
new
BytesWriter(
32
);
String user_id =
"123456"
;
String nickname =
"周江海"
;
bw.writeUTF(user_id);
bw.writeUTF(username);
bw.writeUTF(nickname);
bw.writeInt(i);
bw.writeShort(s);
bw.writeLong(l);
byte
[] data = bw.getBytes();
BytesWriter out =
null
;
if
(data.length >=
0xFFFF
) {
out =
new
BytesWriter(data.length +
6
);
out.writeShort(
0xFFFF
);
out.writeInt(data.length);
}
else
{
out =
new
BytesWriter(data.length +
2
);
out.writeShort(data.length);
}
out.write(data);
mDos.write(out.getBytes());
mDos.close();
mDis.close();
mClient.close();
}
}
catch
(IOException e) {
e.printStackTrace();
}
}
}
|
现在验证一下,先启动Server,然后运行Android客户端应用,屏幕中间会出现TextView和一个button,TextView的内容是HelloWorld,点击button就会发送登陆信息给服务端,然后服务端返回数据显示到TextView上。下面分别是服务端的打印消息和客户端的显示截图。
主要需要掌握基本数据类型和字符串跟字节数组的转换,然后按照指定的顺序发送。最重要的是要注意客户端和服务端从字节数组读取和往字节数组写入数据时的顺序要对应。