Android Socket使用TCP协议实现手机投屏

本节主要通过实战来了解Socket在TCP/IP协议中充当的是一个什么角色,有什么作用。通过Socket使用TCP协议实现局域网内手机A充当服务端,手机B充当客户端,手机B连接手机A,手机A获取屏幕数据转化为Bitmap,通过Socket传递个手机B显示。

实现效果:

一、 Socket是什么?

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层TCP/IP 协议来建立 TCP 连接。建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。 

短连接:连接->传输数据->关闭连接:

传统HTTP是无状态的,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。也可以这样说:短连接是指SOCKET连接后发送后接收完数据后马上断开连接。

长连接:连接->传输数据->保持连接 -> 传输数据-> …… ->关闭连接:

长连接指建立SOCKET连接后不管是否使用都保持连接。

什么时候用长连接,短连接?

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。

总之,长连接和短连接的选择要视情况而定。 而我们接下来要实现的手机实时投屏效果使用的就是长连接。

二、Socket的使用:

在使用Socket时,我们会使用到ServiceSocket和Socket,ServerSocket负责绑定IP地址,启动监听端口;Socket负责发起连接操作。连接成功后,双方通过输入和输出流进行同步阻塞式通信。

1、创建TcpServerRunnable TCP服务端

1)由于Android在使用网络通讯时要放在子线程中执行,所以可以将TcpServerRunnable实现Runnable接口。

public class TcpServerRunnable implements Runnable {
    @Override
    public void run() {
       
    }
}

2)在执行run方法里面创建ServiceSocket,ServerSocket内部使用的是TCP协议,如果想要使用UDP协议可以使用DatagramSocket。

private boolean ServerCreate() {
	try {
		serverSocket = new ServerSocket(port, 1);
	} catch (Exception e) {
		e.printStackTrace();
		if (listener != null) {
			listener.onServerClose();
		}
		return false;
	}
	return true;
}

3)创建成功后,开启 while 循环监听TCP服务端是否被客户端连接;

private void ServerRun() {
	if (!ServerCreate()) {
		return;
	}
	while (true) {
		if (!ServerListen()) {
			break;
		}
	}
}

private boolean ServerListen() {
	try {
		socket = serverSocket.accept();
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
	if (listener != null) {
		listener.onServerConnect();
	}
	return true;
}

4)当有客户端连接到服务端时,开启 while 循环,从内存中拿取bitmap(屏幕数据),组装协议数据,发送给客户端。

private void ServerRun() {
	if (!ServerCreate()) {
		return;
	}
	while (true) {
		if (!ServerListen()) {
			break;
		}
		while (ServerIsConnect()) {
			ServerTransmitBitmap();
			ServerSleep(10);
		}
	}
}

private final static byte[] PACKAGE_HEAD = {(byte) 0xFF, (byte) 0xCF, (byte) 0xFA, (byte) 0xBF, (byte) 0xF6, (byte) 0xAF, (byte) 0xFE, (byte) 0xFF};

/**
 * 写入 协议头+投屏bitmap数据
 */
private void ServerTransmitBitmap() {
	try {
		DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
		if (bitmap != null) {
			byte[] bytes = MyUtils.BitmaptoBytes(bitmap);
			dataOutputStream.write(PACKAGE_HEAD);
			dataOutputStream.writeInt(MyUtils.getScreenWidth());
			dataOutputStream.writeInt(MyUtils.getScreenHeight());
			dataOutputStream.writeInt(bytes.length);
			dataOutputStream.write(bytes);
		}
		dataOutputStream.flush();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

当执行到dataOutputStream.flush();时,就会将数据发送给客户端,由于ServerTransmitBitmap()方法是在 while 循环里面的,所以当手机屏幕数据刷新后,重新赋值给bitmap,服务端会自动将新的bitmap再次组装发送给客户端,实现投屏实时刷新的效果。

public class TcpServerRunnable implements Runnable {

    private static final String TAG = "TcpServerRunnable";

    private ServerSocket serverSocket;
    private Socket socket;
    private int port;
    private Bitmap bitmap;


    public void setPort(int port) {
        this.port = port;
    }

    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    @Override
    public void run() {
        ServerRun();
    }

    /**
     * 运行服务端
     */
    private void ServerRun() {
        if (!ServerCreate()) {
            return;
        }
        while (true) {
            if (!ServerListen()) {
                break;
            }
            while (ServerIsConnect()) {
                ServerTransmitBitmap();
                ServerSleep(10);
            }
        }
    }

    /**
     * 使用ServerSocket创建TCP服务端
     *
     * @return
     */
    private boolean ServerCreate() {
        try {
            serverSocket = new ServerSocket(port, 1);
        } catch (Exception e) {
            e.printStackTrace();
            if (listener != null) {
                listener.onServerClose();
            }
            return false;
        }
        return true;
    }

    /**
     * 循环监听服务端是否被连接
     *
     * @return
     */
    private boolean ServerListen() {
        try {
            socket = serverSocket.accept();
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        if (listener != null) {
            listener.onServerConnect();
        }
        return true;
    }

    /**
     * 判断服务端是否被连接
     *
     * @return
     */
    private boolean ServerIsConnect() {
        return socket != null && !socket.isClosed() && socket.isConnected();
    }

    private final static byte[] PACKAGE_HEAD = {(byte) 0xFF, (byte) 0xCF, (byte) 0xFA, (byte) 0xBF, (byte) 0xF6, (byte) 0xAF, (byte) 0xFE, (byte) 0xFF};

    /**
     * 写入 协议头+投屏bitmap数据
     */
    private void ServerTransmitBitmap() {
        try {
            DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
            if (bitmap != null) {
                byte[] bytes = MyUtils.BitmaptoBytes(bitmap);
                dataOutputStream.write(PACKAGE_HEAD);
                dataOutputStream.writeInt(MyUtils.getScreenWidth());
                dataOutputStream.writeInt(MyUtils.getScreenHeight());
                dataOutputStream.writeInt(bytes.length);
                dataOutputStream.write(bytes);
            }
            dataOutputStream.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void ServerSleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 关闭连接
     */
    public void close() {
        ServerClose();
    }

    private void ServerClose() {
        try {
            if (socket != null) {
                socket.close();
                serverSocket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (listener != null) {
            listener.onServerClose();
        }
    }

    private ServerListener listener;

    public void setListener(ServerListener listener) {
        this.listener = listener;
    }

    public interface ServerListener {
        void onServerConnect();

        void onServerClose();
    }
}

2、获取手机屏幕数据,并转化为bitmap

1)创建ScreenCaptureService前台服务,执行处理捕获设备屏幕的单例类ScreenCapture。

public class ScreenCaptureService extends Service {

    private ScreenCapture screenCapture;

    public ScreenCaptureService() {
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel(); //创建通知栏,你正在录屏
        }
        Bundle bundle = intent.getExtras();
        if (bundle != null) {
            int resultCode = bundle.getInt("resultCode");
            Intent data = bundle.getParcelable("resultData");
            screenCapture = ScreenCapture.getInstance(this, resultCode, data);
        }
        screenCapture.startScreenCapture();
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        screenCapture.stopScreenCapture();
    }

    private void createNotificationChannel() {
       
    }
}

2)创建MediaProjection,捕获设备屏幕上的内容,并将数据转化为Bitmap,MyUtils.setBitmap(bitmap) 存储到静态变量中。

public class ScreenCapture implements ImageReader.OnImageAvailableListener{

    private static final String TAG = "ScreenCapture";

    private final MediaProjection mMediaProjection; // 用于捕获设备屏幕上的内容并进行录制或截图
    private VirtualDisplay mVirtualDisplay;
    private final ImageReader mImageReader;
    private final int screen_width;
    private final int screen_height;
    private final int screen_density;
    private static volatile ScreenCapture screenCapture;

    @SuppressLint("WrongConstant")
    private ScreenCapture(Context context, int resultCode, Intent data) {
        MediaProjectionManager mMediaProjectionManager = (MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE);
        mMediaProjection = mMediaProjectionManager.getMediaProjection(resultCode, data);
        screen_width = MyUtils.getScreenWidth();
        screen_height = MyUtils.getScreenHeight();
        screen_density = MyUtils.getScreenDensity();
        mImageReader = ImageReader.newInstance(
                screen_width,
                screen_height,
                PixelFormat.RGBA_8888,
                2);
    }

    public static ScreenCapture getInstance(Context context, int resultCode, Intent data) {
        if(screenCapture == null) {
            synchronized (ScreenCapture.class) {
                if(screenCapture == null) {
                    screenCapture = new ScreenCapture(context, resultCode, data);
                }
            }
        }
        return screenCapture;
    }

    public void startScreenCapture() {
        if (mMediaProjection != null) {
            setUpVirtualDisplay();
        }
    }

    private void setUpVirtualDisplay() {
        mVirtualDisplay = mMediaProjection.createVirtualDisplay(
                "ScreenCapture",
                screen_width,
                screen_height,
                screen_density,
                DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
                mImageReader.getSurface(),
                null,
                null);

        mImageReader.setOnImageAvailableListener(this, null);
    }

    public void stopScreenCapture() {
        if (mVirtualDisplay != null) {
            mVirtualDisplay.release();
        }
    }

    @Override
    public void onImageAvailable(ImageReader imageReader) {
        try {
            Image image = imageReader.acquireLatestImage();
            if(image != null) {
                Image.Plane[] planes = image.getPlanes();
                ByteBuffer buffer = planes[0].getBuffer();
                int pixelStride = planes[0].getPixelStride();
                int rowStride = planes[0].getRowStride();
                int rowPadding = rowStride - pixelStride * screen_width;
                Bitmap bitmap = Bitmap.createBitmap(screen_width + rowPadding / pixelStride, screen_height, Bitmap.Config.ARGB_8888);
                bitmap.copyPixelsFromBuffer(buffer);
                MyUtils.setBitmap(bitmap);
                image.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3、对获取的屏幕Bitmap进行压缩,降低Bitmap大小,加快Socket传输速度。(非必须,不压缩也行)。创建BitmapProcessRunnable实现Runnable接口,在子线程执行bitmap压缩操作。

public class BitmapProcessRunnable implements Runnable {

    private static final String TAG = "BitmapProcessRunnable";

    private boolean isRun = false;

    public void setRun(boolean isRun) {
        this.isRun = isRun;
    }

    @Override
    public void run() {
        while (isRun) {
            try {
                Bitmap bitmap = MyUtils.getBitmap();
                Log.i(TAG, "bitmap:" + bitmap);
                if (bitmap != null) {
                    bitmap = MyUtils.BitmapMatrixCompress(bitmap);
                    if (listener != null) {
                        listener.onProcessBitmap(bitmap);
                    }
                }
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private ProcessListener listener;

    public interface ProcessListener {
        void onProcessBitmap(Bitmap bitmap);
    }

    public void setListener(ProcessListener listener) {
        this.listener = listener;
    }
}
public static Bitmap BitmapMatrixCompress(Bitmap bitmap) {
	Matrix matrix = new Matrix();
	matrix.setScale(0.5f, 0.5f);
	return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
}

3、关联TCP客户端ServerFragment页面

1)在ServerFragment页面创建TcpServerRunnable(TCP服务端)和BitmapProcessRunnable(图片处理线程),当用户点击创建按钮时,调用MyUtils.ExecuteRunnable(tcpServerRunnable),执行TcpServerRunnable的run方法。

2)客户端连接TCP服务端后,在回调接口onServerConnect()里面,开启前台服务ScreenCaptureService,捕获屏幕数据,同时执行BitmapProcessRunnable的run方法,对获取到的bitmap进行压缩,压缩完成,将新bitmap赋值给TcpServerRunnable(TCP服务端)。

3)TcpServerRunnable(TCP服务端)的 while 循环里面读取到新的bitmap,进行组装bitmap协议数据,发送给客户端。

public class ServerFragment extends Fragment implements View.OnClickListener {

    private static final String TAG = "ServerFragment";

    private static boolean server_create = false;

    private TextView server_text;
    private Button create_button;

    private TcpServerRunnable tcpServerRunnable; // TCP服务端
    private BitmapProcessRunnable bitmapProcessRunnable; // 图片处理线程

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_server, container, false);
    }

    @Override
    public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        server_text = view.findViewById(R.id.server_text);
        server_text.setText(MyUtils.getLocalAddress());

        create_button = view.findViewById(R.id.create_button);
        create_button.setOnClickListener(this);

        bitmapProcessRunnable = new BitmapProcessRunnable();
        bitmapProcessRunnable.setListener(bitmapProcessListener);

        tcpServerRunnable = new TcpServerRunnable();
        tcpServerRunnable.setPort(MyUtils.tcpSocketPort);
        tcpServerRunnable.setListener(tcpServerListener);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.create_button) {
            if (!server_create) {
                server_create = true;
                MyUtils.ExecuteRunnable(tcpServerRunnable);
                create_button.setText("关闭");
            } else {
                server_create = false;
                tcpServerRunnable.close();
                create_button.setText("创建");
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        tcpServerRunnable.close();
    }

    TcpServerRunnable.ServerListener tcpServerListener = new TcpServerRunnable.ServerListener() {
        @Override
        public void onServerConnect() {
            bitmapProcessRunnable.setRun(true);
            // 压缩图片
            MyUtils.ExecuteRunnable(bitmapProcessRunnable);
            Bundle bundle = new Bundle();
            Intent start = new Intent(getActivity(), ScreenCaptureService.class);
            bundle.putInt("resultCode", MyUtils.getResultCode());
            bundle.putParcelable("resultData", MyUtils.getResultData());
            start.putExtras(bundle);
            // 启动投屏服务
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                getActivity().startForegroundService(start);
            } else {
                getActivity().startService(start);
            }
        }

        @Override
        public void onServerClose() {
            bitmapProcessRunnable.setRun(false);
            Intent stop = new Intent(getActivity(), ScreenCaptureService.class);
            getActivity().stopService(stop);
        }
    };

    BitmapProcessRunnable.ProcessListener bitmapProcessListener = new BitmapProcessRunnable.ProcessListener() {
        @Override
        public void onProcessBitmap(Bitmap bitmap) {
            // 将压缩后的图片传给tcpServer,tcpServer发送给客户端
            tcpServerRunnable.setBitmap(bitmap);
        }
    };
}

4、创建TcpClientRunnable TCP客户端

1)同样在子线程中开启连接TCP服务端,这里需要知道服务端的IP地址和端口号。

@Override
public void run() {
	ClientRun();
}

private void ClientRun() {
	if (!ClientConnect()) {
		return;
	}
}

private boolean ClientConnect() {
	try {
		socket = new Socket(ip, port);
	} catch (Exception e) {
		e.printStackTrace();
		return false;
	}
	if (listener != null) {
		listener.onClientConnect();
	}
	return true;
}

2)连接成功后,开启 while 循环,接收服务端发送过来的bitmap数据,赋值给静态变量MyUtils.setBitmap()。

private void ClientRun() {
	if (!ClientConnect()) {
		return;
	}
	while (true) {
		while (ClientIsConnect()) {
			ClientReceiveBitmap();
			ClientSleep(10);
		}
	}
}

private void ClientReceiveBitmap() {
	try {
		Log.i(TAG,"循环读取服务端传过来的投屏Bitmap");
		InputStream inputStream = socket.getInputStream();
		boolean isHead = true;
		for (byte b : PACKAGE_HEAD) {
			byte head = (byte) inputStream.read();
			if (head != b) {
				isHead = false;
				break;
			}
		}
		if (isHead) {
			DataInputStream dataInputStream = new DataInputStream(inputStream);
			int width = dataInputStream.readInt();
			int height = dataInputStream.readInt();
			int len = dataInputStream.readInt();
			byte[] bytes = new byte[len];
			dataInputStream.readFully(bytes, 0, len);
			Bitmap bitmap = MyUtils.BytestoBitmap(bytes);
			if (bitmap != null && width != 0 && height != 0) {
				if (listener != null) {
					listener.onClientReceiveBitmap(bitmap, width, height);
				}
			}
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}

5、客户端创建显示投屏的DisplayActivity

1)DisplayActivity通过自定义DisplayView来实时显示投屏数据;

public class DisplayActivity extends AppCompatActivity {

    private static final String TAG = "DisplayActivity";
    private DisplayView displayView;

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

    private void initView() {
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        displayView = findViewById(R.id.displayView);
        displayView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                displayView.setViewWidth(displayView.getWidth());
                displayView.setViewHeight(displayView.getHeight());
                displayView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
    }

}

2)DisplayView继承自SurfaceView,并实现Runnable接口,当DisplayView添加到Activity后,Surface第一次被创建时回调到void surfaceCreated(),然后在surfaceCreated方法里面启动自己的run方法,循环的将bitmap绘制到页面上。

public class DisplayView extends SurfaceView implements SurfaceHolder.Callback,  Runnable{

    private static final String TAG = "DisplayView";

    private int viewWidth;
    private int viewHeight;
    private Bitmap bitmap;
    private int width;
    private int height;
    private SurfaceHolder surfaceHolder;
    private boolean isDraw = false;

    public DisplayView(Context context) {
        super(context);
        initView();
    }

    public DisplayView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public DisplayView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    private void initView() {
        surfaceHolder = getHolder();
        surfaceHolder.addCallback(this);
        surfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
        setZOrderMediaOverlay(true);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        isDraw = true;
        MyUtils.ExecuteRunnable(this);
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        isDraw = false;
    }

    @Override
    public void run() {
        while (isDraw) {
            try {
                drawBitmap();
                Thread.sleep(10);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    public void drawBitmap() {
        Canvas canvas = surfaceHolder.lockCanvas();
        if (canvas != null) {
            bitmap = getBitmap();
            if (bitmap != null) {
                canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
                Rect rect = new Rect(0, 0, viewWidth, viewHeight);
                canvas.drawBitmap(bitmap, null, rect, null);
            }
            surfaceHolder.unlockCanvasAndPost(canvas);
        }
    }

    public Bitmap getBitmap() {
        // return bitmap;
        return MyUtils.getShowBitmap();
    }

    public void setBitmap(Bitmap bitmap, int width, int height) {
        this.bitmap = bitmap;
        this.width = width;
        this.height = height;
    }

    public void setViewWidth(int width) {
        this.viewWidth = width;
    }

    public void setViewHeight(int height) {
        this.viewHeight = height;
    }
}

6、关联TCP客户端ClientFragment页面

1)在ClientFragment页面创建TcpClientRunnable(TCP客户端端)当用户输入服务端IP地址,点击连接按钮时,调用MyUtils.ExecuteRunnable(tcpClientRunnable),执行TcpClientRunnable的run方法。

2)客户端连接TCP服务端后,在回调接口onClientConnect()里面,开启DisplayActivity,DisplayView创建,等待bitmap写入MyUtils.setBitmap,循环的将bitmap绘制到页面上。

3)当客户端接收到服务端发送过来的投屏bitmap时,回调到onClientReceiveBitmap(),将bitmap设置到静态变量里面MyUtils.setBitmap(bitmap, width, height)。

public class ClientFragment extends Fragment implements View.OnClickListener {

    private static final String TAG = "ClientFragment";

    private static boolean client_connect = false;

    private TextView client_edit;
    private Button connect_button;

    private TcpClientRunnable tcpClientRunnable;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_client, container, false);
    }

    @Override
    public void onViewCreated(@NonNull @NotNull View view, @Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        client_edit = view.findViewById(R.id.client_edit);
        client_edit.setText(MyUtils.getLocalIp());

        connect_button = view.findViewById(R.id.connect_button);
        connect_button.setOnClickListener(this);

        tcpClientRunnable = new TcpClientRunnable();
        tcpClientRunnable.setPort(MyUtils.tcpSocketPort);
        tcpClientRunnable.setListener(tcpClientListener);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.connect_button) {
            if (client_edit.getText().toString().trim().length() == 0) {
                Toast.makeText(getActivity(), "请输入服务端IP", Toast.LENGTH_SHORT).show();
            } else {
                if (!client_connect) {
                    client_connect = true;
                    tcpClientRunnable.setIp(client_edit.getText().toString().trim());
                    MyUtils.ExecuteRunnable(tcpClientRunnable);
                    client_edit.setText(MyUtils.getLocalIp());
                    client_edit.setEnabled(false);
                    connect_button.setText("断开");
                } else {
                    client_connect = false;
                    tcpClientRunnable.close();
                    client_edit.setText(client_edit.getText().toString().trim());
                    client_edit.setEnabled(true);
                    connect_button.setText("连接");
                }
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        tcpClientRunnable.close();
    }

    TcpClientRunnable.ClientListener tcpClientListener = new TcpClientRunnable.ClientListener() {
        @Override
        public void onClientConnect() {
            Log.i(TAG,"TCP连接成功,跳转DisplayActivity");
            Intent intent = new Intent(getActivity(), DisplayActivity.class);
            startActivity(intent);
        }

        @Override
        public void onClientClose() {
            ToastUtils.showShort("TCP连接断开!");
        }

        @Override
        public void onClientReceiveBitmap(Bitmap bitmap, int width, int height) {
            MyUtils.setBitmap(bitmap, width, height);
        }
    };
}

至此,通过Socket实现的手机局域网投屏软件完成。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/75630.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

校园外卖小程序怎么做

校园外卖小程序是为满足校园内学生和教职员工的外卖需求而开发的一种应用程序。它涵盖了从用户端、商家端、骑手端、电脑管理员到小票打印、多商户入驻等多个方面的功能,以下将逐一介绍。 1. 用户端功能:校园外卖小程序为用户提供了便捷的订餐和外卖服务…

21.0 CSS 介绍

1. CSS层叠样式表 1.1 CSS简介 CSS(层叠样式表): 是一种用于描述网页上元素外观和布局的样式标记语言. 它可以与HTML结合使用, 通过为HTML元素添加样式来改变其外观. CSS使用选择器来选择需要应用样式的元素, 并使用属性-值对来定义这些样式.1.2 CSS版本 CSS有多个版本, 每个…

《测试设计思想》——图书推荐

前言: 在当今软件行业飞速发展的时代,软件测试的重要性日益凸显。为了帮助读者提高测试效率和测试质量,清华大学出版社推出了一本名为《测试设计思想》的书籍,由知名专家周海旭老师撰写。这本书深入探讨了测试设计的思想和方法&am…

easyx图形库基础:3实现弹球小游戏

实现弹球小游戏 一.实现弹球小游戏:1.初始化布:2.初始化一个球的信息:3.球的移动和碰撞反弹4.底边挡板的绘制和移动碰撞重置数据。 二.整体代码: 一.实现弹球小游戏: 1.初始化布: int main() {initgraph(800, 600);setorigin(40…

P16 电路定理——巧妙-灵性-智慧

1、诺顿定理的证明 诺顿定理的证明, 回忆戴维南定理的证明是,在a,b两端加上一个电流源,再根据叠加定理,就解电压Uab。 对偶原理: 在a,b两端加上一个电压源u,再根据叠加定理求A中的独立源作用是给到a&#x…

【爬虫】P1 对目标网站的背景调研(robot.txt,advanced_search,builtwith,whois)

对目标网站的背景调研 检查 robot.txt估算网站大小识别网站所用技术寻找网站的所有者 检查 robot.txt 目的: 大多数的网站都会包含 robot.txt 文件。该文件用于指出使用爬虫爬取网站时有哪些限制。而我们通过读 robot.txt 文件,亦可以最小化爬虫被封禁的…

观察者模式实战

场景 假设创建订单后需要发短信、发邮件等其它的操作,放在业务逻辑会使代码非常臃肿,可以使用观察者模式优化代码 代码实现 自定义一个事件 发送邮件 发送短信 最后再创建订单的业务逻辑进行监听,创建订单 假设后面还需要做其它的…

循环内的try-catch 跟循环外的try-catch有什么不一样

起因:一位面试管突然问了这么一道基础的面试题,反而秀了面试者一脸,经常用的却被问到时不知道怎么回答,所以我们平时在写代码的时候,要多注意细节跟原理。也许你不服:不就是先这样,再那样&#…

探讨uniapp的navigator 页面跳转问题

navigator 页面跳转。该组件类似HTML中的<a>组件&#xff0c;但只能跳转本地页面。目标页面必须在pages.json中注册。 "tabBar": {"color": "#7A7E83","selectedColor": "#3cc51f","borderStyle": "bl…

在阿里云服务器上安装Microsoft SharePoint 2016流程

本教程阿里云百科分享如何在阿里云ECS上搭建Microsoft SharePoint 2016。Microsoft SharePoint是Microsoft SharePoint Portal Server的简称。SharePoint Portal Server是一个门户站点&#xff0c;使得企业能够开发出智能的门户站点。 目录 背景信息 步骤一&#xff1a;添加…

TypeScript入门指南

TypeScript学习总结内容目录&#xff1a; TypeScript概述 TypeScript特性。Javascript与TypeScript的区别 * TypeScript安装及其环境搭建TypeScript类型声明 * 单个类型声明&#xff0c;多个类型声明 * 任意类型声明 * 函数类型声明 * unknown类型…

步入React正殿 - State进阶

目录 扩展学习资料 State进阶知识点 状态更新扩展 shouldComponentUpdate PureComponent 为何使用不变数据【保证数据引用不会出错】 单一数据源 /src/App.js /src/components/listItem.jsx 状态提升 /src/components/navbar.jsx /src/components/listPage.jsx src/A…

机器学习:特征工程之特征预处理

目录 特征预处理 1、简述 2、内容 3、归一化 3.1、鲁棒性 3.2、存在的问题 4、标准化 ⭐所属专栏&#xff1a;人工智能 文中提到的代码如有需要可以私信我发给你&#x1f60a; 特征预处理 1、简述 什么是特征预处理&#xff1a;scikit-learn的解释&#xff1a; provide…

07 - 查看、创建、切换和删除分支

查看所有文章链接&#xff1a;&#xff08;更新中&#xff09;GIT常用场景- 目录 文章目录 1. 查看分支2. 创建和切换分支3. 删除分支 1. 查看分支 git branch -va2. 创建和切换分支 第一种&#xff1a; 创建分支&#xff1a; git branch new_branch切换分支&#xff1a; …

辨析:热功率 轴功率

热功率 反应堆热工里提供的裂变反应堆的释放热 堆芯裂变 反应堆能通过高压蒸汽对外输出的总功率值。 反应堆热功率 轴功率 反应堆输出的蒸汽热能&#xff0c;通过机电系统&#xff0c;能转换成推进轴系&#xff0c;加载到推进螺旋桨上的最大实用功率值。 轴功率是输出的机械…

SCF金融公链新加坡启动会 创新驱动未来

新加坡迎来一场引人瞩目的金融科技盛会&#xff0c;SCF金融公链启动会于2023年8月13日盛大举行。这一受瞩目的活动将为金融科技领域注入新的活力&#xff0c;并为广大投资者、合作伙伴以及关注区块链发展的人士提供一个难得的交流平台。 在SCF金融公链启动会上&#xff0c; Wil…

Rust语法:所有权引用生命周期

文章目录 所有权垃圾回收管理内存手动管理内存Rust的所有权所有权转移函数所有权传递 引用与借用可变与不可变引用 生命周期悬垂引用函数生命周期声明结构体的生命周期声明Rust生命周期的自行推断生命周期约束静态生命周期 所有权 垃圾回收管理内存 Python&#xff0c;Java这…

yolov8训练进阶:自定义训练脚本,从配置文件载入训练超参数

yolov8官方教程提供了2种训练方式&#xff0c;一种是通过命令行启动训练&#xff0c;一种是通过写代码启动。 命令行的方式启动方便&#xff0c;通过传入参数可以方便的调整训练参数&#xff0c;但这种方式不方便记录训练参数和调试训练代码。 自行写训练代码的方式更灵活&am…

logstash 原理(含部署)

1、ES原理 原理 使⽤filebeat来上传⽇志数据&#xff0c;logstash进⾏⽇志收集与处理&#xff0c;elasticsearch作为⽇志存储与搜索引擎&#xff0c;最后使⽤kibana展现⽇志的可视化输出。所以不难发现&#xff0c;⽇志解析主要还 是logstash做的事情 从上图中可以看到&#x…

将CNKI知网文献条目导出,并导入到Endnote内

将CNKI知网文献条目导出&#xff0c;并导入到Endnote内 目录 将CNKI知网文献条目导出&#xff0c;并导入到Endnote内一、从知网上导出参考文献二、将知网导出的参考文献导入到Endnote 一、从知网上导出参考文献 从知网上导出参考文献过程和步骤如图1所示。 图1 导出的参考文献…