1. 程式人生 > >Android(客戶端)通過socket與QT(服務端)通訊

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());    
                    }    
                }    
            }   
        }    
    }    
}