Android(客戶端)通過socket與QT(服務端)通信

一、概述

在這裏我想實現一個跨平臺的socket通訊,Android手機作爲客戶端向Ubuntu的QT平臺上的服務端發送一個字符命令,由於是隻發送一個字符,這裏我儘可能簡化socket通訊的過程以供後人參考。
文中貼上主要代碼,末尾會給出完整源代碼的下載。

二、QT的服務端

QT上的服務端我使用了QTcpServer和QTcpSocket類,大體的流程是這樣的:
1、主窗口進入UI
2、啓動TcpServer開始監聽一個端口
3、監聽到有新的Connection信號則觸發下一個函數獲取該socket
4、獲取到該socket後觸發讀函數槽
5、讀取信息,並進行字符編碼的轉換(很重要)
上代碼
mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <sys/socket.h>
#include <sys/types.h>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    startTcpserver();
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::startTcpserver()
{
    m_tcpServer = new QTcpServer(this);
    m_tcpServer->listen(QHostAddress::Any,60000); //監聽任何連上60000端口的ip
    connect(m_tcpServer,SIGNAL(newConnection()),this,SLOT(newConnect())); //新連接信號觸發,調用newConnect()槽函數,這個跟信號函數一樣,可以隨便取。
}

void MainWindow::newConnect()
{
    m_tcpSocket = m_tcpServer->nextPendingConnection(); //得到每個連進來的socket
    connect(m_tcpSocket,SIGNAL(readyRead()),this,SLOT(readMessage())); //有可讀的信息,觸發讀函數槽       
}

void MainWindow::readMessage() //讀取信息
{
    qint64 len = m_tcpSocket->bytesAvailable();
    qDebug()<<"socket data len:"<< len;
    QByteArray alldata = m_tcpSocket->read(len);
    //開始轉換編碼
    QTextCodec *utf8codec = QTextCodec::codecForName("UTF-8");
    QString utf8str = utf8codec->toUnicode(alldata.mid(2));
    qDebug()<<"hex:["<<alldata.toHex().toUpper()<<"]";
    qDebug()<<"utf-8 ["<< (utf8str) << "]";
    //顯示到控件上
    ui->label->setText(utf8str);

}

void MainWindow::sendMessage() //發送信息
{
    //QString strMesg= ui->lineEdit_sendmessage->text();
    QString strMesg="連接成功";
    qDebug()<<strMesg;
    m_tcpSocket->write(strMesg.toStdString().c_str(),strlen(strMesg.toStdString().c_str())); //發送
}

這是主窗口源代碼,所有函數都在這裏,函數調用過程是ui->startTcpserver()->newConnect()->readMessage()。
由於從java發過來的String類型字符串在socket傳輸過程中實際上被轉換成UTF-8編碼的字節數組,QT作爲Server接收之後要對其進行轉換,就是readMessage()函數裏的過程

    qint64 len = m_tcpSocket->bytesAvailable();//獲取長度
    qDebug()<<"socket data len:"<< len;
    QByteArray alldata = m_tcpSocket->read(len);
    /**開始轉換編碼**/
    QTextCodec *utf8codec = QTextCodec::codecForName("UTF-8");
    QString utf8str = utf8codec->toUnicode(alldata.mid(2));
    qDebug()<<"hex:["<<alldata.toHex().toUpper()<<"]";
    qDebug()<<"utf-8 ["<< (utf8str) << "]";
    ui->label->setText(utf8str);//顯示到控件上

在QT用QByteArray字節數組接收java發過來的String轉換而來的字節數組,最終解包成QString類型的字符串得以在QT上顯示

三、Android的客戶端

在Android 4.0之後網絡操作這樣耗時的操作只能放在子線程中實現,所以在Android代碼中我簡單的創建了一個子線程來實現socket發送字符,下面這段代碼就是子線程:

    private void sendData(){
        try{
            Socket socket = new Socket("192.168.1.112",60000);//創建Socket實例,並綁定連接服務端的IP地址和端口
            Log.e("線程反饋","創建成功!");
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());          
            out.writeUTF(command); //以UTF的方式發送字符command
            socket.close();//一定記得關閉socket
            button_status.setText(command);//按鈕上顯示被髮送的字符            
        }catch(Exception e){
            Log.e("線程反饋","線程異常!");          
        }
    }

值得注意的是客戶端和服務端必須在同一局域網或者在電腦是用 ping 命令嘗試連接手機的IP,如果可以ping的通,才能保證客戶端和服務端能夠正常通信。
其實創建socket併發送字符核心的就下面四句話:

Socket socket = new Socket("192.168.1.112",60000);
DataOutputStream out = new DataOutputStream(socket.getOutputStream());  
out.writeUTF(command); 
socket.close();

我的手機客戶端設置了幾個按鈕用來發送對應的控制命令字符
手機客戶端界面
下面是Android主要代碼:
MainActivity.java

package com.example.test_socket;

import java.io.DataOutputStream;
import java.net.Socket;
import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;

public class MainActivity extends Activity implements OnClickListener{
    private Button button_left;
    private Button button_right;
    private Button button_up;
    private Button button_down;
    private Button button_stop;
    private Button button_start;
    private Button button_status;
    private String command;//按鈕發送的命令

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);//指定了當前活動的佈局,這裏表示將從res/layout目錄中找到activity_main.xml文件作爲本例的佈局文件使用。
        button_left=(Button)findViewById(R.id.button_left);
        button_right=(Button)findViewById(R.id.button_right);
        button_up=(Button)findViewById(R.id.button_up);
        button_down=(Button)findViewById(R.id.button_down);
        button_start=(Button)findViewById(R.id.button_start);
        button_stop=(Button)findViewById(R.id.button_stop);
        button_status=(Button)findViewById(R.id.button_status);//顯示被髮送的命令
        button_left.setOnClickListener(this); //監聽按鍵 
        button_right.setOnClickListener(this); //監聽按鍵 
        button_up.setOnClickListener(this); //監聽按鍵 
        button_down.setOnClickListener(this); //監聽按鍵 
        button_start.setOnClickListener(this); //監聽按鍵 
        button_stop.setOnClickListener(this); //監聽按鍵 
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
        return true;
    }

    @Override
    public void onClick(View arg0) {
        switch (arg0.getId()){
            case R.id.button_left:
                command = "L";
                break;
            case R.id.button_right:
                command = "R";
                break;
            case R.id.button_up:
                command = "U";
                break;
            case R.id.button_down:
                command = "D";
                break;
            case R.id.button_start:
                command = "B";
                break;
            case R.id.button_stop:
                command = "E";  
                break;
            default:
                command = " ";//在按了其他按鍵的情況下命令置爲空格
                break;
        }
        new Thread(){
            @Override
            public void run(){
                sendData();//啓動子線程創建socket併發送字符
            }
        }.start();              
    }

    private void sendData(){
        try{
            Socket socket = new Socket("192.168.1.112",60000);//創建Socket實例,並綁定連接遠端IP地址和端口
            Log.e("線程反饋","創建成功!");
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());          
            out.writeUTF(command); 
            socket.close();
            button_status.setText(command);
            /*OutputStream ops = socket.getOutputStream();//定義一個輸出流,來自於Socket輸出流
            String b="a\n";
            byte[] bytes = b.getBytes();                    
            ops.write(bytes);//向輸出流中寫入數據    
            Log.v("線程反饋","發送成功!");
            ops.flush();//刷行輸出流 
             */ 
        }catch(Exception e){
            Log.e("線程反饋","線程異常!");          
        }
    }
}

四、要注意的幾點

1、服務端和客戶端必須能夠Ping通才能保證正常通信
2、Android端設置的IP地址一定要是服務端的IP,端口號一定要和服務端監聽的端口一致
Android客戶端:

Socket socket = new Socket("192.168.1.112",60000);

QT服務端:

m_tcpServer->listen(QHostAddress::Any,60000);

3、如果連接成功,手機按一個控制按鈕最下面一排第三個按鈕會顯示那個被按下的字符
4、建議調試Android端程序的時候開啓手機裏開發者選項的USB調試,直接連接手機進行調試,因爲在模擬器裏調試會遇到其他問題。
5、我調試的時候跑Ubuntu的電腦和跑Android的手機是連在同一個無線路由上的,保證他們在同一個局域網下可以ping通
6、由於跨平臺傳輸存在字符編碼轉換的問題,請仔細考慮上面的readMessage()函數
7、如果想先測試一下Android客戶端能否與電腦建立socket連接可以用java寫一個服務端程序做測試,但要注意電腦上開啓監聽某一個端口之後一定要正常關閉程序否則會出現程序關閉端口未被關閉導致端口占用的情況。
下面給出一個java服務端測試代碼:
(此處要感謝Defonds的博客 一個 Java 的 Socket 服務器和客戶端通信的例子這兩個例子代碼很簡潔,也很實用)
Server.java

import java.io.BufferedReader;  
import java.io.DataInputStream;  
import java.io.DataOutputStream;  
import java.io.InputStreamReader;  
import java.net.ServerSocket;  
import java.net.Socket;  

public class Server {  
    public static final int PORT = 60000;//監聽的端口號     

    public static void main(String[] args) {    
        System.out.println("服務器啓動...\n");    
        Server server = new Server();    
        server.init();    
    }    

    public void init() {    
        try {    
            ServerSocket serverSocket = new ServerSocket(PORT);    
            while (true) {    
                // 一旦有堵塞, 則表示服務器與客戶端獲得了連接    
                Socket client = serverSocket.accept();    
                // 處理這次連接    
                new HandlerThread(client);    
            }    
        } catch (Exception e) {    
            System.out.println("服務器異常: " + e.getMessage());    
        }    
    }    

    private class HandlerThread implements Runnable {    
        private Socket socket;    
        public HandlerThread(Socket client) {    
            socket = client;    
            new Thread(this).start();    
        }    

        public void run() {    
            try {    
                // 讀取客戶端數據    
                DataInputStream input = new DataInputStream(socket.getInputStream());  
                String clientInputStr = input.readUTF();//這裏要注意和客戶端輸出流的寫方法對應,否則會拋 EOFException  
                // 處理客戶端數據    
                System.out.println("客戶端發過來的內容:" + clientInputStr);    

                // 向客戶端回覆信息    
                DataOutputStream out = new DataOutputStream(socket.getOutputStream());                         
                out.close();    
                input.close();    
            } catch (Exception e) {    
                System.out.println("服務器 run 異常: " + e.getMessage());    
            } finally {    
                if (socket != null) {    
                    try {    
                        socket.close();    
                    } catch (Exception e) {    
                        socket = null;    
                        System.out.println("服務端 finally 異常:" + e.getMessage());    
                    }    
                }    
            }   
        }    
    }    
}    

sublime編輯器裏直接配置好java編譯環境可以直接Ctrl+B編譯運行開始監聽。
完整代碼包下載:
http://download.csdn.net/detail/u013453604/9017403
有問題請回複評論

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章