Android中基于ServerSocket的实际使用与简单介绍(内附一个PC端群控多台手机的消息发送和接收Demo)

一、要想将ServerSocket整明白首先至少先要知道或是了解几点基础内容部分(大神或是有一定能力的小神跳过):

1.ServerSocket是需要在服务端定义书写的而在客户端不需要ServerSocket,客户端只需要创建socket就可以了。

2.socket需要在子线程中处理,所以必须开辟子线程,而老版本的socket不需要在子线程中书写,所以网上的一些比较老版本的帖子Demo什么的没有创建子线程,这点需要注意。

3.在Android Studio中创建Java代码,控制台会出现中文乱码的情况(与UTF-8转码无关),我在另一个博客文章中介绍了解决方法(转载的文章),出现乱码的同学可以去找一下。

4.数据的发送与接收都需要通过socket来读取或发送输入输出流。

5.子线程不能改变主线程的UI,所以需要使用Handler,因为比较简单所以以上这些就不过多的讲解了。


二、为了更好的说明和理解Socket通信,我将查到的一些可能常出现的异常和使用方法等进行了总结:

异常类型
在了解Socket的内容之前,先要了解一下涉及到的一些异常类型。以下四种类型都是继承于IOException,所以很多之后直接弹出IOException即可。
UnkownHostException:      主机名字或IP错误
ConnectException:        服务器拒绝连接、服务器没有启动、(超出队列数,拒绝连接)
SocketTimeoutException:      连接超时
BindException:          Socket对象无法与制定的本地IP地址或端口绑定


构造函数
Socket(InetAddress address, int port)throws UnknownHostException, IOException
Socket(InetAddress address, int port, InetAddress localAddress, int localPort)throws IOException
Socket(String host, int port)throws UnknownHostException, IOException
Socket(String host, int port, InetAddress localAddress, int localPort)throws IOException
 

除去第一种不带参数的之外,其它构造函数会尝试建立与服务器的连接。如果失败会抛出IOException错误。如果成功,则返回Socket对象。
InetAddress是一个用于记录主机的类,其静态getHostByName(String msg)可以返回一个实例,其静态方法getLocalHost()也可以获得当前主机的IP地址,并返回一个实例。Socket(String host, int port, InetAddress localAddress, int localPort)构造函数的参数分别为目标IP、目标端口、绑定本地IP、绑定本地端口。
 

Socket方法
getInetAddress();       远程服务端的IP地址
getPort();          远程服务端的端口
getLocalAddress()       本地客户端的IP地址
getLocalPort()        本地客户端的端口
getInputStream();       获得输入流
getOutStream();        获得输出流
值得注意的是,在这些方法里面,最重要的就是getInputStream()和getOutputStream()了。
 
Socket状态
isClosed();             //连接是否已关闭,若关闭,返回true;否则返回false
isConnect();            //如果曾经连接过,返回true;否则返回false
isBound();             //如果Socket已经与本地一个端口绑定,返回true;否则返回false
如果要确认Socket的状态是否处于连接中,下面语句是很好的判断方式。
boolean isConnection=socket.isConnected() && !socket.isClosed();   //判断当前是否处于连接


/**
 * 1.获得远程服务器的IP 地址.
 * InetAddress inetAddress = socket.getInetAddress();
 * 2.获得远程服务器的端口.
 * int port = socket.getPort();
 * 3. 获得客户本地的IP 地址.
 * InetAddress localAddress = socket.getLocalAddress();
 * 4.获得客户本地的端口.
 * int localPort = socket.getLocalPort();
 * 5.获取本地的地址和端口号
 * SocketAddress localSocketAddress = socket.getLocalSocketAddress();
 * 6.获得远程的地址和端口号
 * SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress();
 */

以上的代码是自己找到或是自己整理的关于能更好的加深对socket了解的一些知识点段落,在开始看下面代码之前最好先了解一下。


三、为了更好的进行了解和总结,并且尽量避免和网上的一些Demo博客冲突,所以自己写了两个Demo(一个在PC端中是Java项目暂且叫D1,另一个在手机端是Android项目暂且叫D2)大致功能为:1)一台PC能够连接多台手机进行数据的发送和接收;2)PC端可以显示已连接手机的数量以及ID端口号;3)手机端和PC端的数据发送会得到返回值来表示是否发送接收成功;4)实现群发和单独发送;都是一些比较小的功能,但却是一个大项目的雏形,所以我进行了一个简单的封装来尽量节省代码量和方便后期的修改。话不多说首先介绍D1也就是PC端的Java代码如下所示:

1.创建Java工程(还是尽量讲的详细些吧~微笑):


 

2.创建一个ClientManager的客户端管理类,实现如下代码:

// 管理连接到服务器中的手机类
public class ClientManager {

    private static ServerThread serverThread = null;
    private static int sum = 0;
    private static Map<String, Socket> clientMap = new HashMap<>();
    private static List<String> clientList = new ArrayList<>();

    private static class ServerThread implements Runnable {

        private ServerSocket server;
        private int port = 10086;
        private boolean isExit = false;// 一个boolean类型的判断 默认是退出状态false

        // 构造方法初始化
        public ServerThread() {
            try {
                server = new ServerSocket(port);
                System.out.println("启动server,端口号:" + port);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 1.获得远程服务器的IP 地址.
         * InetAddress inetAddress = socket.getInetAddress();
         * 2.获得远程服务器的端口.
         * int port = socket.getPort();
         * 3. 获得客户本地的IP 地址.
         * InetAddress localAddress = socket.getLocalAddress();
         * 4.获得客户本地的端口.
         * int localPort = socket.getLocalPort();
         * 5.获取本地的地址和端口号
         * SocketAddress localSocketAddress = socket.getLocalSocketAddress();
         * 6.获得远程的地址和端口号
         * SocketAddress remoteSocketAddress = socket.getRemoteSocketAddress();
         */
        @Override
        public void run() {
            try {
                while (!isExit) {
                    // 等待连接
                    System.out.println("等待手机的连接中... ...");
                    final Socket socket = server.accept();
                    System.out.println("获取的手机IP地址及端口号:" + socket.getRemoteSocketAddress().toString());
                    /**
                     * 因为考虑到多手机连接的情况 所以加入线程锁 只允许单线程工作
                     */
                    new Thread(new Runnable() {

                        private String text;

                        @Override
                        public void run() {
                            try {
                                synchronized (this) {
                                    // 在这里考虑到线程总数的计算 也代表着连接手机的数量
                                    ++sum;
                                    // 存入到集合和Map中为群发和单独发送做准备
                                    String string = socket.getRemoteSocketAddress().toString();
                                    clientList.add(string);
                                    clientMap.put(string, socket);
                                }

                                // 定义输入输出流
                                InputStream is = socket.getInputStream();
                                OutputStream os = socket.getOutputStream();

                                // 接下来考虑输入流的读取显示到PC端和返回是否收到
                                byte[] buffer = new byte[1024];
                                int len;
                                while ((len = is.read(buffer)) != -1) {
                                    text = new String(buffer, 0, len);

                                    System.out.println("收到的数据为:" + text);
                                    os.write("已收到消息".getBytes("utf-8"));

                                }

                            } catch (IOException e) {
                                e.printStackTrace();
                            } finally {
                                System.out.println("关闭连接:" + socket.getRemoteSocketAddress().toString());
                                synchronized (this) {
                                    --sum;
                                    String string = socket.getRemoteSocketAddress().toString();
                                    clientMap.remove(string);
                                    clientList.remove(string);
                                }
                            }
                        }
                    }).start();

                }

            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        // 关闭server
        public void stop() {
            isExit = true;
            if (server != null) {
                try {
                    server.close();
                    System.out.println("已关闭server");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    // 启动server
    public static ServerThread startServer() {
        System.out.println("开启server");
        if (serverThread != null) {
            System.out.println("server不为null正在重启server");
            // 以下为关闭server和socket
            shutDown();
        }
        // 初始化
        serverThread = new ServerThread();
        new Thread(serverThread).start();
        System.out.println("开启server成功");
        return serverThread;
    }


    // 发送消息的方法
    public static boolean sendMessage(String name, String mag) {
        try {
            Socket socket = clientMap.get(name);
            OutputStream outputStream = socket.getOutputStream();
            outputStream.write(mag.getBytes("utf-8"));
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    // 群发的方法
    public static boolean sendMsgAll(String msg){
        try {
            for (Socket socket : clientMap.values()) {
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write(msg.getBytes("utf-8"));
            }
                return true;
        }catch (Exception e){
            e.printStackTrace();
        }
        return false;
    }

    // 获取线程总数的方法,也等同于<获取已连接了多少台手机>的方法+
    public static int sumTotal() {
        return sum;
    }

    // 一个获取list集合的方法,取到所有连接server的手机的ip和端口号的集合
    public static List<String> getTotalClients() {
        return clientList;
    }

    public static void shutDown() {
        for (Socket socket : clientMap.values()) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        serverThread.stop();
        clientMap.clear();
        clientList.clear();
    }


}
因为这个博客的代码编辑不是特别的会用,所以直接从我的TestDemo中直接复制了~但是我已经尽可能多的每段都敲好了注释,所以还有什么不懂的地方可以google一下~或者是运行一下代码用debug模式看下代码的执行流程~到这里为止我觉的只需要注意如下几个方面:

(1)socket = serverSocket.accept();像牛郎织女一样~一直在苦苦等待自己要等的人出现...如果不出现就一直等下去,直到等到符合条件的请求出现为止,才会分配出一个socket来让程序向下执行,不然一直持续在阻塞等待状态下(不信你打log试试看)

(2)使用完之后一定记得要关闭server和socket (比关流要重要的多)在代码中会有大量的判断要考虑,我写的还不是很成熟很多地方还没有考虑完美,但是能实现想要的结果。

(3)synchronized的使用在于优化对线程的控制,因为是单排车道通过,所以可以从中获取通过了多少个子线程(也就意味着获取了连接手机的数量),因为每台手机的IP及端口号都不一致,所以通过这个来存储到Map中来实现单独指定手机发送数据的功能,存到Map中的key为IP端口号、value为分配的socket,从而可以通过遍历Map来实现群发的功能,所以synchronized是不可或缺的。


3.到MyClass中,这个名字是创建工程的时候默认哦~所以不要说我命名不规范~

public class MyClass {

    public static void main(String[]args) throws IOException {

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line;
        boolean isExit = false;

        ClientManager.startServer();
        while (!isExit){
            line = br.readLine();
            if (line.startsWith("exit")){
                System.out.println("退出命令");
                break;
            }

            if (line.startsWith("send")){
                sendMessage(line);
            }else if (line.startsWith("list")){
                printTotal();
            }else if (line.startsWith("all")){
                allSendMsg(line);
            }else {
                System.out.println("输入错误 请重新输入");
            }

        }
        // 关闭 清空
        ClientManager.shutDown();

    }

    private static void allSendMsg(String line) {
        String[] field = line.split("//");
        if (field.length == 2){
            ClientManager.sendMsgAll(field[1]);
            System.out.println("发送结果为:" + ClientManager.sendMsgAll(field[1]) );
        }else {
            System.out.println("格式不正确 例:all//message");
        }
    }

    private static void printTotal() {
        List<String> totalClients = ClientManager.getTotalClients();
        System.out.println("连接数量为:" + totalClients.size());
        for (String totalClient : totalClients) {
            System.out.println(totalClient);
        }
    }

    private static void sendMessage(String line) {
        String[] field = line.split("//");
        if (field.length == 3){
            // 格式正确
            ClientManager.sendMessage(field[1],field[2]);
            System.out.println("send结果为:" + ClientManager.sendMessage(field[1],field[2]));
        }else {
            System.out.println("命令不正确。例子:send//name//msg");
        }
    }

}
已上的这些代码我觉的应该没啥大问题,因为自己写的东西实在是有点浅薄... 敲代码其实有时就像是在写书法一样,有的人代码打开会让你看的陶醉和钦佩,而有的人的代码就让人感觉不舒服跟看小学生的字帖儿一样..我可能就是后者吧 = =' 有不懂的地方可以自己google一下~我就不多说明了 大笑


4.接下来是创建D2,也就是Android项目,首先我建议先把权限加上不要忘了:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

5.Android项目我就没有封装,直接想写啥就写啥了,但是代码不多很方便理解,如下所示:

public class MainActivity extends AppCompatActivity {

    private Button btn;
    private EditText et;
    private TextView tv;
//    private WifiManager w = null;
    private Socket socket;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        btn = (Button) findViewById(R.id.btn);
        et = (EditText) findViewById(R.id.et_send);
        tv = (TextView) findViewById(R.id.tv_js);

        final Handler handler = new MyHandler();


        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    socket = new Socket("192.168.1.111", 10086);
                    // 接收
                    InputStream inputStream = socket.getInputStream();
                    byte[] buffer = new byte[102400];
                    int len;
                    while ((len = inputStream.read(buffer)) != -1) {
                        String s = new String(buffer, 0, len);
                        //
                        Message message = Message.obtain();
                        message.what = 0;
                        message.obj = s;
                        handler.sendMessage(message);
                    }


                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String string = et.getText().toString();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            // 发送
                            OutputStream outputStream = null;
                            outputStream = socket.getOutputStream();
                            outputStream.write(("IP:" + getHostIp() + " " + string).getBytes("utf-8"));
                            outputStream.flush();// 清空缓存

                        } catch (IOException e) {
                            e.printStackTrace();
                        }

                    }
                }).start();
            }
        });

    }

// 获取IP并转换格式
    private String getHostIp() {

        WifiManager mg = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        if (mg == null){
            return "";
        }

        WifiInfo wifiInfo = mg.getConnectionInfo();
        int ip = wifiInfo.getIpAddress();
        return ((ip & 0xff) + "." + (ip >> 8 & 0xff) + "."
                + (ip >> 16 & 0xff) + "." + (ip >> 24 & 0xff));
    }

    private class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            if (msg.what == 0) {
                String s = (String) msg.obj;
                tv.setText(s);
            }
        }

    }
}


以上代码值得注意的是如下几点:

1)不能再子线程中刷新UI,通过Handler来发送msg;

2)通过WifiManager来获取IP结果需要转换格式,就好像系统的日期是一串数字要通过转换格式变成我们能看懂的样式一样(那串转换格式的代码我也是从网上Ctrl + C、Ctrl + V来的偷笑

3)socket = new Socket("192.168.1.111", 10086);中第一个参数填的是PC端的IP地址,第二个参数要与PC中Server设置的端口号一致;

4)如果wifi环境不好,可以像下图代码一样设置超时时间来进行其它操作:

socket = new Socket();
SocketAddress socAddress = new InetSocketAddress(dsName, dstPort);
// 设置超时时间
socket.connect(socAddress, 5000);
Log.i(TAG, "Connect OK!");


6.Android端的XML文件代码如下图所示:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="sq.can_26_socket.MainActivity">

    <TextView
        android:id="@+id/tv_js"
        android:layout_margin="10dp"
        android:textSize="20sp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/et_send"
        android:hint="@string/xx"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/btn"
        android:layout_marginTop="5dp"
        android:text="@string/send"
        android:layout_gravity="center"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>


上方的TextView用来显示从PC端接收到的数据,EditText用来获取发送到PC端的消息。综上所有就可以运行你的代码啦~先运行PC端的Java代码,在运行Android项目到手机中,可以把APK打包到多台手机进行测试。最后测试的效果如下图所示:

1)首先运行PC端操作台会有如下的显示结果:



2)打开手机中的项目后,操作台会显示(连接数量的上线我不知道是多少):



3)在操作台上输入如下绿色的指令后进行群发操作,向两台手机发送“这是群发的消息”,显示如下:



4)手机接收到的消息后,显示的效果如下图所示:



5)手机向电脑发送消息后,操作台显示的效果如下图所示(显示发送消息的手机的IP地址和Message内容):


6)向指定手机发送消息,操作台上编辑如下图绿色部分的内容,执行发送命令:



7)手机上接收后显示效果如下所示:


总结:综上所述就是Server Socket的简单介绍和部分应用场景的Demo,我将这两个Demo上传到Guthub上,有需要的同学可以点击<这里这里这里>进行下载~ 如果哪里有错误欢迎批评指正~如果对我有什么建议可以留言交流~我会一一回复的~下篇博客要写啥腻... ... 有点不知所以然啊,看着那些大神大牛的博客,感觉都是好高深呀~好像不太适合接触Android不久的我和一些新手朋友们,所以就像我的签名一样,希望通过一篇篇的博客来逐渐的走向大牛或是架构师的人生巅峰之中~~



  • 12
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
实现一对多通信需要使用TCP/IP协议,Winsock库提供了相应的API函数来实现网络通信。以下是一个简单的基于Winsock的TCP/IP客户端和服务器端程序的示例代码,可以实现多个客户端连接到服务器并发送消息,同时服务器可以向所有客户端广播消息。 服务器端代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #pragma comment(lib,"ws2_32.lib") //加载 ws2_32.dll #define PORT 8888 //服务器监听端口号 int main() { WSADATA wsaData; SOCKET serverSocket, clientSocket; SOCKADDR_IN serverAddr, clientAddr; int addrLen = sizeof(SOCKADDR_IN); char recvBuf[1024]; int recvLen; char sendBuf[1024]; int sendLen; int i; int clientCount = 0; SOCKET clientSockets[10]; //初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed!\n"); return 1; } //创建套接字 serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (serverSocket == INVALID_SOCKET) { printf("socket failed!\n"); WSACleanup(); return 1; } //绑定套接字到本地地址和端口 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = htonl(INADDR_ANY); serverAddr.sin_port = htons(PORT); if (bind(serverSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("bind failed!\n"); closesocket(serverSocket); WSACleanup(); return 1; } //监听连接请求 if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) { printf("listen failed!\n"); closesocket(serverSocket); WSACleanup(); return 1; } printf("Server started, waiting for clients...\n"); while (1) { //接受连接请求 clientSocket = accept(serverSocket, (SOCKADDR*)&clientAddr, &addrLen); if (clientSocket == INVALID_SOCKET) { printf("accept failed!\n"); continue; } //将客户端套接字加入数组 clientSockets[clientCount] = clientSocket; clientCount++; //打印客户端地址和端口号 printf("Client %s:%d connected.\n", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); //向所有客户端广播有新客户端连接 sprintf(sendBuf, "Client %s:%d connected.", inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port)); for (i = 0; i < clientCount - 1; i++) { sendLen = send(clientSockets[i], sendBuf, strlen(sendBuf) + 1, 0); } } //关闭所有客户端套接字 for (i = 0; i < clientCount; i++) { closesocket(clientSockets[i]); } //关闭服务器套接字 closesocket(serverSocket); //清理Winsock库 WSACleanup(); return 0; } ``` 客户端代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <winsock2.h> #pragma comment(lib,"ws2_32.lib") //加载 ws2_32.dll #define SERVER_IP "127.0.0.1" //服务器IP地址 #define PORT 8888 //服务器监听端口号 int main() { WSADATA wsaData; SOCKET clientSocket; SOCKADDR_IN serverAddr; char recvBuf[1024]; int recvLen; char sendBuf[1024]; int sendLen; //初始化Winsock库 if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed!\n"); return 1; } //创建套接字 clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (clientSocket == INVALID_SOCKET) { printf("socket failed!\n"); WSACleanup(); return 1; } //连接到服务器 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = inet_addr(SERVER_IP); serverAddr.sin_port = htons(PORT); if (connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { printf("connect failed!\n"); closesocket(clientSocket); WSACleanup(); return 1; } //向服务器发送消息 sprintf(sendBuf, "Hello from client %d.", GetCurrentProcessId()); sendLen = send(clientSocket, sendBuf, strlen(sendBuf) + 1, 0); //接收服务器消息 recvLen = recv(clientSocket, recvBuf, sizeof(recvBuf), 0); printf("Received from server: %s\n", recvBuf); //关闭套接字 closesocket(clientSocket); //清理Winsock库 WSACleanup(); return 0; } ``` 以上代码只是基础示例,还需要进行错误处理和优化。同时如果要支持多个客户端,可以使用线程或者IOCP模型来处理多个客户端的连接和消息传输。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值