您的位置:首页 > 移动开发

基于Socket编程的远程控制PC音乐播放器App(二)

2017-02-03 17:44 543 查看
三、客户端

具体的布局文件就不细说了,可以看源码,这里主要列出主要实现代码。

1、网络字节转换工具类(重要)

要想实现Java和C/C++的通信,客户端得先把数据全部转换为byte数组,int、float、double类型的转换必须进行特殊处理,这里只封装了int、String的转换,其他可根据需要自行封装。

public class NetDataTransformUtils {

/**
* int 转为 ByteArray
* 将int转为低字节在前,高字节在后的byte数组
* @param n
* @return
*/
public static byte[] intToByteArray(int n) {
byte[] b = new byte[4];
b[0] = (byte) (n & 0xff);
b[1] = (byte) (n >> 8 & 0xff);
b[2] = (byte) (n >> 16 & 0xff);
b[3] = (byte) (n >> 24 & 0xff);
return b;
}

/**
* ByteArray 转为 int
* @param bArr
* @return
*/
public static int byteArrayToInt(byte[] bArr) {
int n = 0;
for(int i=0;i<bArr.length&&i<4;i++){
int left = i*8;
n+= (bArr[i] << left);
}
return n;
}

/**
* ByteArray 转为 String
* @param valArr
* @param maxLen
* @return
*/
public static String byteArrayToString(byte[] valArr,int maxLen) {
String result=null;
int index = 0;
while(index < valArr.length && index < maxLen) {
if(valArr[index] == 0) {
break;
}
index++;
}
byte[] temp = new byte[index];
System.arraycopy(valArr, 0, temp, 0, index);
try {
result= new String(temp,"GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}

/**
* String 转为 ByteArray
* @param str
* @return
*/
public static byte[] stringToByteArray(String str){
byte[] temp = null;
try {
temp = str.getBytes("GBK");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return temp;
}
}


2、首部类

public class PktHeader{
private int pktLen;
private int pktType;

public PktHeader(){

}

public PktHeader(byte[] b){
if(b.length == 4){
byte[] b1 = new byte[4];
int i;
for (i=0;i<4;i++) {
b1[i] = b[i];
}
this.pktLen = NetDataTransformUtils.byteArrayToInt(b1);
}else if(b.length >= 8){
byte[] b1 = new byte[4];
byte[] b2 = new byte[4];
int i;
for (i=0;i<4;i++) {
b1[i] = b[i];
}
this.pktLen = NetDataTransformUtils.byteArrayToInt(b1);
for (i=0;i<4;i++) {
b2[i] = b[4+i];
}
this.pktType = NetDataTransformUtils.byteArrayToInt(b2);
}

}

public PktHeader(int pktLen, int pktType){
this.pktLen = pktLen;
this.pktType = pktType;
}

public int getPktLen() {
return pktLen;
}

public void setPktLen(int pktLen) {
this.pktLen = pktLen;
}

public int getPktType() {
return pktType;
}

public void setPktType(int pktType) {
this.pktType = pktType;
}
}


3、数据类(列举两个典型: 发送数据类、响应数据类)

(1)发送数据类

每次向服务器发送这个类对象前,先转换为byte数组,再发送。

public class LoginPkt{
private PktHeader header;
private String username;
private String password;

public LoginPkt(){

}

public LoginPkt(PktHeader header, String username, String password){
this.header = header;
this.username = username;
this.password = password;
}

public PktHeader getHeader() {
return header;
}

public void setHeader(PktHeader header) {
this.header = header;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public byte[] getBuf() {
byte[] buf = new byte[4+this.getHeader().getPktLen()];
byte[] temp = NetDataTransformUtils.intToByteArray(this.getHeader().getPktLen());
System.arraycopy(temp, 0, buf, 0, 4);

temp = NetDataTransformUtils.intToByteArray(this.getHeader().getPktType());
System.arraycopy(temp, 0, buf, 4, 4);

temp = NetDataTransformUtils.stringToByteArray(this.getUsername());
System.arraycopy(temp, 0, buf, 8, 20);

byte[] temp2 = NetDataTransformUtils.stringToByteArray(this.getPassword());
System.arraycopy(temp2, 0, buf, 28, temp2.length);

return buf;
}
}


(2)响应数据类

每次接收到byte数组,先转换为Java中的变量类型,再使用。

public class LoginReplyPkt {
PktHeader header;
int ret;

public LoginReplyPkt(){

}

public LoginReplyPkt(byte[] b){
byte[] b1 = new byte[4];
byte[] b2 = new byte[4];
byte[] b3 = new byte[4];
int i;
for (i=0;i<4;i++) {
b1[i] = b[i];
}
for (i=0;i<4;i++) {
b2[i] = b[4+i];
}
for (i=0;i<4;i++) {
b3[i] = b[8+i];
}
this.header = new PktHeader(NetDataTransformUtils.byteArrayToInt(b1), NetDataTransformUtils.byteArrayToInt(b2));
this.ret = NetDataTransformUtils.byteArrayToInt(b3);
}

public PktHeader getHeader() {
return header;
}

public void setHeader(PktHeader header) {
this.header = header;
}

public int getRet() {
return ret;
}

public void setRet(int ret) {
this.ret = ret;
}
}


4、Socket操作线程类

(1)发送数据,直接调用DataOutputStream.write()即可。

(2)接收数据,为了避免粘包、丢包,先接受数据包首部的长度字段,然后根据长度接收数据,最后组合成一条数据。

如果可自动进行IP获取,那最好不过了。

public class SocketThread extends Thread {

private static final String addr = "117.23.249.158"; // 自己设置同一局域网IP
private static final int port = 8888;
private int timeout = 3000;
private Socket socket;

DataInputStream dataInputStream;
DataOutputStream dataOutputStream;

Handler outHandler;
Handler inHandler;

public boolean isRun = true;

public SocketThread(){

}

public SocketThread(Handler outHandler, Handler inHandler){
this.outHandler = outHandler;
this.inHandler = inHandler;
}

/**
* 连接
*/
public void connect(){
try {
socket = new Socket();
socket.connect(new InetSocketAddress(addr, port), timeout);
if(socket.isConnected()){
dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataInputStream = new DataInputStream(socket.getInputStream());
}
}catch (UnknownHostException e) {
e.printStackTrace();
connect();
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 接收数据
*/
@Override
public void run() {
connect();
while(isRun) {
try {
if (socket.isConnected()) {
//接收头部包长
byte[] recvBufHeader = new byte[4];
dataInputStream.read(recvBufHeader, 0, 4);
PktHeader header = new PktHeader(recvBufHeader);
//根据包长接收具体数据
int pktLen = header.getPktLen();
byte[] recvBuf = new byte[pktLen];
dataInputStream.read(recvBuf, 0, pktLen);
//合成数据
byte[] data = new byte[4+pktLen];
System.arraycopy(recvBufHeader, 0, data, 0, 4);
System.arraycopy(recvBuf, 0, data, 4, pktLen);

Message message = inHandler.obtainMessage();
message.what = 1;
message.obj = data;
inHandler.sendMessage(message);
} else {
connect();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

/**
* 发送数据
* @param data
*/
public void send(byte[] data){
try {
if(socket.isConnected()) {
dataOutputStream.write(data);
dataOutputStream.flush();

Message message = outHandler.obtainMessage();
message.what = 1;
outHandler.sendMessage(message);
}else{
Message message = outHandler.obtainMessage();
message.what = 0;
outHandler.sendMessage(message);
}
} catch (IOException e) {
Message message = outHandler.obtainMessage();
message.what = 0;
outHandler.sendMessage(message);
e.printStackTrace();
}
}

/**
* 关闭资源
*/
public void close(){
try{
if(socket != null){
isRun = false;
dataOutputStream.close();
dataInputStream.close();
socket.close();
}
}catch (IOException e){
e.printStackTrace();
}
}
}


5、功能实现

1、登录

public class MainActivity extends Activity {

@BindView(R.id.main_username)
EditText mainUsername;
@BindView(R.id.main_password)
EditText mainPassword;

ProgressDialog progressDialog;

SocketThread loginThread;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);

startSocket();
progressDialog = new ProgressDialog(MainActivity.this);
}

public Handler outHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 0:
ToastUtils.showToast(MainActivity.this, "网络错误");
break;
case 1:
progressDialog.setMessage("正在登录...");
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
break;
}
}
};

public Handler inHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 1:
LoginReplyPkt loginReplyPkt = new LoginReplyPkt((byte[]) msg.obj);
if (loginReplyPkt.getHeader().getPktType() == PktType.PKT_LOGIN_REPLY && loginReplyPkt.getRet() == PktType.LOGIN_SUCCESS) {
progressDialog.dismiss();
ToastUtils.showToast(MainActivity.this, "登录成功");
loginThread.isRun = false;
// 登录成功跳转
Bundle bundle = new Bundle();
bundle.putString("username",mainUsername.getText().toString().trim());
Intent goMusicPlay = new Intent(getApplicationContext(), MusicPlayActivity.class);
goMusicPlay.putExtras(bundle);
startActivity(goMusicPlay);
finish();
} else {
progressDialog.dismiss();
ToastUtils.showToast(MainActivity.this, "登录失败");
}
break;
}

super.handleMessage(msg);
}
};

public void startSocket(){
loginThread = new SocketThread(outHandler, inHandler);
loginThread.start();
}

public void login() {
String usernameStr = mainUsername.getText().toString().trim();
String passwordStr = mainPassword.getText().toString().trim();

if (!TextUtils.isEmpty(usernameStr) && !TextUtils.isEmpty(passwordStr)) {
//构建登录包
LoginPkt loginPkt = new LoginPkt();
if (usernameStr.length() < 20) {
for (int i = usernameStr.length(); i < 20; i++) {
usernameStr += '\0';
}
}
loginPkt.setUsername(usernameStr);
loginPkt.setPassword(passwordStr);
loginPkt.setHeader(new PktHeader(4+20+20, PktType.PKT_LOGIN));

loginThread.send(loginPkt.getBuf());
}
}

@OnClick(R.id.main_login)
public void onClick(View v) {
switch (v.getId()) {
case R.id.main_login:
login();
break;
}
}

@Override
protected void onDestroy() {
if(progressDialog != null){
progressDialog.dismiss();
}
super.onDestroy();
}
}


2、获取音乐列表,播放、停止音乐

注意,为了避免重复多次播放,我定义了一个集合Map<String, Integer> playMap,记录每首歌的播放状态。

public class MusicPlayActivity extends AppCompatActivity {

ActionBar actionBar;
@BindView(R.id.toolbar_title)
TextView toolbarTitle;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.music_play_tips)
TextView musicPlayTips;

private String username;

@BindView(R.id.music_play_list_view)
ListView musicPlayListView;
List<Music> data = new ArrayList<>();
MusicAdapter musicAdapter;

TextView musicName;
ImageView startStop;
Map<String, Integer> playMap = new HashMap<>(); //每条音乐的播放
int[] playImage = {R.drawable.music_play_start, R.drawable.music_play_stop};

SocketThread musicThread;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music_play);
ButterKnife.bind(this);

initActionBar();
initListView();

if (getIntent().getExtras() != null) {
username = getIntent().getExtras().getString("username");
}

startSocket();
}

public void initActionBar() {
// ToolBar
toolbar.setTitleTextColor(getResources().getColor(R.color.white));
// 设置标题
toolbarTitle.setText("音乐台");
setSupportActionBar(toolbar);

actionBar = getSupportActionBar();
if (actionBar != null) {
// 去掉 ActionBar 自带标题
actionBar.setTitle(null);
}
}

public void initListView() {
if(data.size() > 0){
musicPlayTips.setVisibility(View.GONE);
musicPlayListView.setVisibility(View.VISIBLE);
}else{
musicPlayTips.setVisibility(View.VISIBLE);
musicPlayListView.setVisibility(View.GONE);
}
musicAdapter = new MusicAdapter(data, this);
musicPlayListView.setAdapter(musicAdapter);
musicPlayListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
startStop = (ImageView) view.findViewById(R.id.music_item_music_start_stop);
musicName = (TextView) view.findViewById(R.id.music_item_music_name);

playStartStop(musicName.getText().toString().trim());
}
});
}

public void startSocket() {
musicThread = new SocketThread(outHandler, inHandler);
musicThread.start();
}

public Handler outHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 0:
ToastUtils.showToast(MusicPlayActivity.this, "网络错误");
break;
}
}
};

public Handler inHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
PktHeader header = new PktHeader((byte[]) msg.obj);
switch (header.getPktType()) {
case PktType.PKT_GET_MUSIC_LIST_REPLY:
GetMusicListReplyPkt pkt = new GetMusicListReplyPkt((byte[]) msg.obj);
String name = getFileNameNoEx(pkt.getMusicName());
musicAdapter.addItem(new Music(name));
playMap.put(name, 0);
if(data.size() > 0){
musicPlayTips.setVisibility(View.GONE);
musicPlayListView.setVisibility(View.VISIBLE);
}else{
musicPlayTips.setVisibility(View.VISIBLE);
musicPlayListView.setVisibility(View.GONE);
}
break;
case PktType.PKT_PLAY_START_STOP_REPLY:
PlayStartStopReplyPkt replyPkt = new PlayStartStopReplyPkt((byte[]) msg.obj);
if (replyPkt.getRet() == PktType.PLAY_START_SUCCESS) {
playMap.put(musicName.getText().toString().trim(), 1);
startStop.setImageResource(playImage[1]);
ToastUtils.showToast(MusicPlayActivity.this, "播放成功!");
}
if (replyPkt.getRet() == PktType.PLAY_START_FAIL) {
ToastUtils.showToast(MusicPlayActivity.this, "播放失败!");
}
if (replyPkt.getRet() == PktType.PLAY_STOP_SUCCESS) {
playMap.put(musicName.getText().toString().trim(), 0);
startStop.setImageResource(playImage[0]);
ToastUtils.showToast(MusicPlayActivity.this, "停止成功!");
}
if (replyPkt.getRet() == PktType.PLAY_STOP_FAIL) {
ToastUtils.showToast(MusicPlayActivity.this, "停止失败!");
}
break;
}
}
};

public void getMusicList() {
// 构建包
GetMusicListPkt pkt = new GetMusicListPkt();
pkt.setHeader(new PktHeader(4, PktType.PKT_GET_MUSIC_LIST));

musicThread.send(pkt.getBuf());
}

/**
* 获取不带文件类型的文件名
* @param filename
* @return
*/
public String getFileNameNoEx(String filename) {
if ((filename != null) && (filename.length() > 0)) {
int dot = filename.lastIndexOf('.');
if ((dot > -1) && (dot < (filename.length()))) {
return filename.substring(0, dot);
}
}
return filename;
}

public void playStartStop(String name) {
for(Map.Entry<String, Integer> entry : playMap.entrySet()){
if(entry.getKey().equals(name)){
if(entry.getValue() == 0){
if(!isPlaying()){
// 构建开始播放包
PlayStartStopPkt pkt = new PlayStartStopPkt();
pkt.setHeader(new PktHeader(4+4+30, PktType.PKT_PLAY_START_STOP));
pkt.setType(PktType.PLAY_START);
pkt.setMusicName(name);
musicThread.send(pkt.getBuf());
}else{
ToastUtils.showToast(getApplicationContext(), "不允许同时播放多首音乐");
}
}else{
// 构建停止播放包
PlayStartStopPkt pkt = new PlayStartStopPkt();
pkt.setHeader(new PktHeader(4+4+30, PktType.PKT_PLAY_START_STOP));
pkt.setType(PktType.PLAY_STOP);
pkt.setMusicName(name);
musicThread.send(pkt.getBuf());
}
}
}
}

/**
* 是否有音乐在播放
* @return
*/
public boolean isPlaying(){
for(Map.Entry<String, Integer> entry : playMap.entrySet()){
if(entry.getValue() == 1){
return true;
}
}
return false;
}

public void logout() {
// 构建包
LogoutPkt pkt = new LogoutPkt();
pkt.setHeader(new PktHeader(4+30, PktType.PKT_LOGOUT));
pkt.setUsername(username);

musicThread.send(pkt.getBuf());
musicThread.close();
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.music_play_menu, menu);
return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.music_play_menu_get_music_list:
if(!isPlaying()){
//每获取一次必须清楚原有数据
musicAdapter.clearAll();
playMap.clear();
getMusicList();
}else {
ToastUtils.showToast(getApplicationContext(), "请先关闭当前播放的音乐");
}
break;
case R.id.music_play_menu_logout:
logout();
finish();
break;
}

return super.onOptionsItemSelected(item);
}

@Override
protected void onDestroy() {
super.onDestroy();
musicThread.close();
}
}


再进行优化,客户端能显示播放进度,体验会更好!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐