Android网络编程(二)Socket编程

前言

上篇文章我们描述了传输层协议TCP、UDP,但它们毕竟只是协议,看不见摸不着,那我们怎们通过TCP、和UDP进行实际传输呢?不用着急,等看完这篇文章你一定会明白的。

1 Socket概述

Socket中文意思为插座的意思,专业术语称之为套接字,它把TCP/IP封装成了调用接口供开发者调用,也就是说开发者可以通过调用Socket相关API来实现网络通讯。在Java中也存在Socket相关API,主要分为两个,分别是基于UDP传输协议的Socket和基于TCP传输协议的Socket,本篇文章会对基于这两种传输协议的Socket进行详细描述。

2 UDP Socket

2.1 基本使用

通过上节的内容我们知道UDP是无连接的,只要提供对方的IP地址和端口号就能进行数据的传输,其中IP负责定位主机端口负责定位应用。知道了目标IP和目标端口号通过Java中的UDP Socket就能进行IO传输,我们来看一下具体的代码体现

**
 * 发送方UDP
 */
public class UDPSocketSend {
    public static void main(String[] args) throws IOException {

        System.out.println("Sender Start...");

        //1.创建socket服务
        DatagramSocket ds = new DatagramSocket();

        //2.封装数据
        String str = "Did you recite words today";
        byte[] bytes = str.getBytes();
        //地址
        InetAddress address =InetAddress.getByName("192.168.31.137");
        //参数:数据、长度、地址、端口
        DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);

        //3.发送数据包
        ds.send(dp);

        //4.关闭socket服务
        ds.close();
    }

/**
 * 接收方UDP
 */
public class UDPSocketReceive{
    public static void main(String[] args) throws IOException {

        System.out.println("Receiver Start...");

        //1.创建udp的socket服务,并声明端口号
        DatagramSocket ds = new DatagramSocket(6666);

        //2.创建接收数据的数据包
        byte[] bytes = new byte[1024];
        DatagramPacket dp = new DatagramPacket(bytes,bytes.length);

        //3.将数据接收到数据包中,为阻塞式方法
        ds.receive(dp);

        //4.解析数据
        InetAddress address = dp.getAddress();//发送方IP
        int port = dp.getPort();//发送方端口
        String content = new String(dp.getData(),0,dp.getLength());
        System.out.println("address:"+address+"---port:"+port+"---content:"+content);

        //关闭服务
        ds.close();
    }
}

由于笔者旁边只有一台电脑,所以我就通过端口号来区分发送方和接收方。
分别启动发送方和接收方,我们来看一下打印结果
发送方:

Sender Start...

接收方

Receiver Start...
address:/192.168.31.137---port:65037---content:Did you recite words today

成功接收到消息,并打印出发送方IP和端口,下面我来个大家撸一遍步骤
发送方:

  • 首先创建udp的socket服务
  • 将需要发送的数据放在数据包DatagramSocket中,DatagramSocket会根据UDP协议对数据包、IP、端口号进行封装
  • 通过udp的socket服务将数据包发送
  • 最后将udp服务关闭

接收方:

  • 创建udp的socket服务,并且明确自己的端口号
  • 创建DatagramSocket用来解析数据接收到的数据包
  • 将数据接收到数据包DatagramSocket中
  • 通过DatagramSocket解析数据
  • 关闭服务

整个UDP发送数据的流程就是这样

注意点:

  • 因为UDP是无连接的不可靠传输,所以接收方需要在发送方发送数据之前就启动,否则会接收不到数据,也就是说必须先运行UDPSocketReceive再运行UDPSocketSend。

2.2 聊天实例

把上面的例子进行一些小改动就可以实现聊天功能

public class UDPSocket1 {
    public static void main(String[] args) {
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        receive();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            send();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //接收消息方法
    private static void receive() throws IOException {
        System.out.println("UDOSocket1 Receiver Start...");

        //1.创建udp的socket服务,并声明端口号
        DatagramSocket ds = new DatagramSocket(6666);
        //无限循环,一直处于接收状态
        while (true) {
            //2.创建接收数据的数据包
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

            //3.将数据接收到数据包中
            ds.receive(dp);

            //4.解析数据
            String content = new String(dp.getData(), 0, dp.getLength());
            System.out.println("UDPSocket1 Receive:" + content);
        }
    }

    private static void send() throws IOException {
        //1.创建socket服务
        DatagramSocket ds = new DatagramSocket();

        //将键盘输入的信息转换成输入流再放入到缓冲区
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        while ((line=br.readLine())!=null){
            //2.封装数据
            byte[] bytes = line.getBytes();
            //地址
            InetAddress address =InetAddress.getByName("192.168.31.137");
            //参数:数据、长度、地址、端口
            DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,7777);

            //3.发送数据包
            ds.send(dp);
        }

        //4.关闭socket服务
        ds.close();
    }
}

public class UDPSocket2 {
    public static void main(String[] args){
        try {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        receive();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            send();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //接收消息方法
    private static void receive() throws IOException {
        System.out.println("UDOSocket2 Receiver Start...");

        //1.创建udp的socket服务,并声明端口号
        DatagramSocket ds = new DatagramSocket(7777);
        //无限循环,一直处于接收状态
        while (true) {
            //2.创建接收数据的数据包
            byte[] bytes = new byte[1024];
            DatagramPacket dp = new DatagramPacket(bytes, bytes.length);

            //3.将数据接收到数据包中
            ds.receive(dp);

            //4.解析数据
            String content = new String(dp.getData(), 0, dp.getLength());
            System.out.println("UDPSocket2 Receive:" + content);
        }
        //关闭服务
//        ds.close();
    }

    private static void send() throws IOException {
        //1.创建socket服务
        DatagramSocket ds = new DatagramSocket();

        //将键盘输入的信息转换成输入流再放入到缓冲区
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String line = null;
        while ((line=br.readLine())!=null){
            //2.封装数据
            byte[] bytes = line.getBytes();
            //地址
            InetAddress address =InetAddress.getByName("192.168.31.137");
            //参数:数据、长度、地址、端口
            DatagramPacket dp = new DatagramPacket(bytes,bytes.length,address,6666);

            //3.发送数据包
            ds.send(dp);
        }

        //4.关闭socket服务
        ds.close();
    }
}

在接收方方法写一个无限循环让其处于持续接收状态,发送发通过对键盘录入加回车进行消息的发送,并且两个端点都具备发送和接收功能。需要注意的是receive()会执行一个无限循环,所以receive()和send()必须位于不同线程,否则会导致无法发送消息从而也接收不到消息。
来看打印结果:
UDPSocket1

UDPSocket Receiver Start...
UDPSocket Receive:hello udp1
heelo udp2

UDPSocket2

UDPSocket2 Receiver Start...
hello udp1
UDPSocket2 Receive:hello udp2

我在UDPSocket1 的控制台中输入了:“hello udp2”、在UDPSocket2 的控制台中输入了:“hello udp1”,接收内容和发送内容完全一致,一个简单的聊天功能就实现了,希望不熟悉的朋友也可以敲一遍代码练一遍。

3 TCP Socket

3.1 基本使用

TCP基于client-server是面向连接的可靠传输,上篇文章我们也讲解了TCP安全传输机制,可谓是相当复杂,如果需要个人对TCP协议进行封装显然大多数开发者是很难实现的,所以Java也为开发者提供了基于TCP的Socket,不同于UDP,TCP Socket分为Socket和ServerSocket对应着client和server,下面我来用代码实现一个简单的TCP通讯功能:
客户端:

//客户端
public class TCPClient {
    public static void main(String[] args) throws IOException {

        //1.创建TCP客户端Socket服务
        Socket client = new Socket();
        //2.与服务端进行连接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10000);
        client.connect(address);
        //3.连接成功后获取客户端Socket输出流
        OutputStream outputStream = client.getOutputStream();
        //4.通过输出流往服务端写入数据
        outputStream.write("hello server".getBytes());
        //5.关闭流
        client.close();
    }
}

首先创建一个Socket和InetSocketAddress ,然后通过Socket的connect()方法进行连接,连接成功后可以获取到输出流,通过该输出流就可以向服务端传输数据。

服务端:

public class TCPServer {
    public static void main(String[] args) throws IOException {
        //1.创建服务端Socket并明确端口号
        ServerSocket serverSocket = new ServerSocket(10000);
        //2.获取到客户端的Socket
        Socket socket = serverSocket.accept();
        //3.通过客户端的Socket获取到输入流
        InputStream inputStream = socket.getInputStream();
        //4.通过输入流获取到客户端传递的数据
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String line = null;
        while ((line = bufferedReader.readLine())!=null){
            System.out.println(line);
        }

        //5.关闭流
        socket.close();
        serverSocket.close();
    }
}

首先创建一个服务端Socket并明确端口号,通过accept()方法获取到链接过来的客户端Socket,从客户端Socket中获取输入流,最后由输入流读取客户端传输来的数据。
我们来看看服务端的打印结果:

hello server

成功接收到客户端发来的数据

注意点:

一个服务端是可以同时和多个客户端进行通信的,那么它是如何区分不同客户端呢?从上面代码我们可以看到,服务端首先通过accept()获取到客户端Socket,然后通过客户端的Socket获取的流进行通讯,这也让服务端得以区分每个客户端。

3.2 文件传输实例

流程:客户端上传一个文件到服务端,服务端收到文件后进行保存,保存成功后给客户端一个响应。
客户端代码

public class TCPUploadClient {

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

        //1.创建TCP客户端Socket服务
        Socket client = new Socket();

        //2.与服务端进行连接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10001);
        client.connect(address);

        //3.读取客户端文件
        FileInputStream fis = new FileInputStream("E://girl.jpg");

        //4.获取输出流
        OutputStream outputStream = client.getOutputStream();

        //5.将文件写入到服务端
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = fis.read(bytes))!=-1){
            outputStream.write(bytes,0,len);
        }

        //6.通知服务器数据写入完毕
        client.shutdownOutput();

        //7.读取服务端响应的数据
        InputStream inputStream = client.getInputStream();
        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
        String line = br.readLine();
        System.out.println(line);

        //8.关闭流
        inputStream.close();
        fis.close();
        client.close();
    }
}

创建Socket并连接,读取本地文件输入流,将文件写入到服务端,写入成功后读取服务端返回的数据。

服务端代码


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

        //1.创建客户端Socket
        ServerSocket serverSocket = new ServerSocket(10001);
        
        //2.获取到客户端Socket
        Socket socket = serverSocket.accept();
        
        //3.通过客户端Socket获取到输入流
        InputStream is = socket.getInputStream();
        
        //4.将流以文件的形式写入到磁盘
        File dir = new File("F://tcp");
        //如果文件夹不存在就创建文件夹
        if(!dir.exists()){
            dir.mkdirs();
        }
        File file = new File(dir,"girl.jpg");
        FileOutputStream fos = new FileOutputStream(file);
        byte[] bytes = new byte[1024];
        int len = 0;
        while ((len = is.read(bytes))!=-1){
            fos.write(bytes,0,len);
        }
        
        //5.通知客户端文件保存完毕
        OutputStream os = socket.getOutputStream();
        os.write("success".getBytes());
        
        //6.关闭流
        fos.close();
        os.close();
        socket.close();
    }
}

创建Socket并获取到客户端Socket和输入流,在F盘的tcp目录下创建一个文件,将从客户端读取到的数据输出到文件中,保存成功后给客户端返回''success''
效果


gil

客户端控制台

success

这样我们就实现了一哥客户端上传文件的功能,还是比较简单的,希望大家也能够跟着代码敲一遍。

3.3 TCP Socket多线程应用

细心的同学可能已经发现,上面我们的实例中一个服务端只能接收一个客户端的一次请求,这显然是不符合实际的,那么如何使一个服务端同时服务多个客户端呢?接着撸代码

//客户端1
public class TCPClient1 {
    public static void main(String[] args) throws IOException {
        System.out.println("TCPClient1 Start...");
        //1.创建TCP客户端Socket服务
        Socket client = new Socket();

        //2.与服务端进行连接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
        client.connect(address);

        //3.连接成功后获取客户端Socket输出流
        OutputStream outputStream = client.getOutputStream();

        //4.通过输出流往服务端写入数据
        outputStream.write("Hello my name is Client1".getBytes());

        //5.告诉服务端发送完毕
        client.shutdownOutput();

        //6.读取服务端返回数据
        InputStream is = client.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));

        //7.关闭流
        client.close();
    }
}

//客户端2
public class TCPClient2 {

    public static void main(String[] args) throws IOException {
        System.out.println("TCPClient2 Start...");

        //1.创建TCP客户端Socket服务
        Socket client = new Socket();

        //2.与服务端进行连接
        InetSocketAddress address = new InetSocketAddress("192.168.31.137",10004);
        client.connect(address);

        //3.连接成功后获取客户端Socket输出流
        OutputStream outputStream = client.getOutputStream();

        //4.通过输出流往服务端写入数据
        outputStream.write("Hello my name is Client2".getBytes());

        //5.告诉服务端发送完毕
        client.shutdownOutput();

        //6.读取服务端返回数据
        InputStream is = client.getInputStream();
        byte[] bytes = new byte[1024];
        int len = is.read(bytes);
        System.out.println(new String(bytes,0,len));

        //7.关闭流
        client.close();
    }
}
//服务端
public class TCPServer {
    public static void main(String[] args) throws IOException {
        receive();
    }

    private static void receive() throws IOException {
        System.out.println("Server Start...");
        //创建服务端Socket并明确端口号
        ServerSocket serverSocket = new ServerSocket(10004);

        while (true){
            //获取到客户端的Socket
            Socket socket = serverSocket.accept();
            //通过客户端的Socket获取到输入流
            InputStream is = socket.getInputStream();

            //通过输入流获取到客户端传递的数据
            byte[] bytes = new byte[1024];
            int len = is.read(bytes);
            System.out.println(new String(bytes,0,len));

            //将客户端发来的数据原封不动返回
            OutputStream os = socket.getOutputStream();
            os.write(new String(bytes,0,len).getBytes());
            //关闭连接
            socket.close();
        }
    }
}

客户端1控制台

TCPClient1 Start...
Hello my name is Client1

客户端2控制台

TCPClient2 Start...
Hello my name is Client2

这样就可以实现一个服务端服务多个客户端

细心的同学可能又发现了,上面的写法是存在问题的,由于服务端始终都在主线程中处理请求,所以客户端的请求需要被服务端排队处理,举个例子:Client1对服务端进行了一次请求,服务端在响应Client1之前是不会接受其他请求的,显然这是不符合逻辑的,真正的服务器是要具备并发处理的。而多线程恰好能解决这个问题,我们来看修改之后的服务端代码

public class TCPServer {
    public static void main(String[] args) throws IOException {
        receive();
    }

    private static void receive() throws IOException {
        System.out.println("Server Start...");

        //创建服务端Socket并明确端口号
        ServerSocket serverSocket = new ServerSocket(10004);
        while (true){
            //获取到客户端的Socket
            final Socket socket = serverSocket.accept();
            //通过线程执行客户端请求
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        //通过客户端的Socket获取到输入流
                        InputStream is = socket.getInputStream();

                        //通过输入流获取到客户端传递的数据
                        byte[] bytes = new byte[1024];
                        int len = is.read(bytes);
                        System.out.println(new String(bytes,0,len));

                        //将客户端发来的数据原封不动返回
                        OutputStream os = socket.getOutputStream();
                        os.write(new String(bytes,0,len).getBytes());
                        //关闭连接
                        socket.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }
}

运行效果适合加线程之前是一样的,但这种方式效率更高。

总结

本篇文章叙述了基于UDP和TCP的Socket,UDP是无连接的,所以UDP Socket在发送数据的时候只需要目标IP和端口即可发送。TCP是面向连接的并且是基于client-server模式,在传输数据前需要进行连接,可以通过多线程技术实现并发处理客户端请求。本篇文章内容还是比较简单的,希望大家能把文章中代码自己敲一遍,掌握Socket的同时也能让你自己UDP和TCP的理解更加深刻。本篇文章对Socket的描述到此为止,下篇文章《Android网络编程(三)HTTP、HTTPS》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 计算机网络概述 网络编程的实质就是两个(或多个)设备(例如计算机)之间的数据传输。 按照计算机网络的定义,通过一定...
    蛋炒饭_By阅读 1,175评论 0 10
  • 网络编程 一.楔子 你现在已经学会了写python代码,假如你写了两个python文件a.py和b.py,分别去运...
    go以恒阅读 1,896评论 0 6
  • 网络编程 网络编程对于很多的初学者来说,都是很向往的一种编程技能,但是很多的初学者却因为很长一段时间无法进入网络编...
    程序员欧阳阅读 1,969评论 1 37
  • 1、TCP为什么需要3次握手,4次断开? “三次握手”的目的是“为了防止已失效的连接请求报文段突然又传送到了服务端...
    杰伦哎呦哎呦阅读 3,439评论 0 6
  • 其一 天暮寒云低,帘动北风凄。 幽林残树色,梧苍霜叶稀。 乱山鸟空啼,愁肠入涧溪。 独怜秋花落,入土化春泥。 其二...
    张庆军看世界阅读 805评论 0 15