接上一篇,已经清楚RTSP推流需要发送的RTSP请求消息,所以我们实现一个RTSP的客户端,完成这些请求消息,并将H264的RTP包推送到指定的RTSP服务器即可,这个客户端同时推送过Darwin和Crtmp-server两个RTSP的服务端,来验证推流客户端的功能实现。

下面是我们实现的RtspPushStreamClient推流客户端类:

package net.majorkernelpanic.streaming.rtmp;import static net.majorkernelpanic.streaming.SessionBuilder.AUDIO_NONE;
import static net.majorkernelpanic.streaming.SessionBuilder.VIDEO_NONE;
import android.hardware.Camera.CameraInfo;
import android.os.Handler;
import android.util.Log; import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;import net.majorkernelpanic.streaming.Session;
import net.majorkernelpanic.streaming.SessionBuilder;
import net.majorkernelpanic.streaming.rtsp.RtspClient;
import net.majorkernelpanic.streaming.rtsp.RtspClient.Callback;/*** Created by aaa on 2015/11/25.*/
public class RtspPushStreamClient implements Callback{private static final String TAG = "RtspPushStreamClient";
//    private static int localPort = 8100;RtspPushClient client;String mSdp;Handler mUserHandler;//public RtspPushStreamClient(Handler handler){client = new RtspPushClient(this); mUserHandler= handler;}public void startRtspClient(String ip, String rtmp_path){  client.start_connect(ip, 554, rtmp_path); }public void switchCamera(){client.switchCamera();}public void setFlash(boolean mode){client.setFlash(mode);}public void stopRtspClient(){client.stop_connect();}public static class RtspPushClient {private RtspClient client;private String request_uri;String mSdp;public  RtspPushClient(RtspPushStreamClient streamClient){client = new RtspClient(); client.setCallback(streamClient);}public void start_connect(String ip, int port, String path){//"rtsp://192.168.0.5:9010/"client.setServerAddress(ip, port);        client.setStreamPath("/"+path);//"/live/rtsp_test"//setCamera --CAMERA_FACING_FRONTSessionBuilder builder = SessionBuilder.getInstance().clone();
//          builder.setCamera(CameraInfo.CAMERA_FACING_FRONT);
//          builder.setAudioEncoder(SessionBuilder.AUDIO_AAC).setVideoEncoder(SessionBuilder.VIDEO_H264);builder.setAudioEncoder(SessionBuilder.AUDIO_AAC).setVideoEncoder(SessionBuilder.VIDEO_H264);SessionBuilder b = SessionBuilder.getInstance();  Random rand = new Random();int localPort = rand.nextInt(1000)+ 8100; //8100-9100localPort = (localPort & 0xFFFE); /* turn to even number */Session session = builder.build(localPort); client.setSession(session);client.startStream(localPort);  }public void switchCamera(){ client.switchCamera();}public void setFlash(boolean mode){client.setFlash(mode);}public void stop_connect(){ client.stopStream();} }@Overridepublic void onRtspUpdate(int message, Exception exception) {// TODO Auto-generated method stubLog.d(TAG, "message:"+message);mUserHandler.sendEmptyMessage(message); }}

RTSPClient的tryConnection方法完成了和服务器的RTSP请求交互:

/** Copyright (C) 2011-2014 GUIGUI Simon, fyhertz@gmail.com* * This file is part of Spydroid (http://code.google.com/p/spydroid-ipcamera/)* * Spydroid is free software; you can redistribute it and/or modify* it under the terms of the GNU General Public License as published by* the Free Software Foundation; either version 3 of the License, or* (at your option) any later version.* * This source code is distributed in the hope that it will be useful,* but WITHOUT ANY WARRANTY; without even the implied warranty of* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the* GNU General Public License for more details.* * You should have received a copy of the GNU General Public License* along with this source code; if not, write to the Free Software* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA*/package net.majorkernelpanic.streaming.rtsp;import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;import net.majorkernelpanic.streaming.Session;
import net.majorkernelpanic.streaming.Stream;
import net.majorkernelpanic.streaming.video.VideoStream;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Locale;
import java.util.concurrent.Semaphore;
import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** RFC 2326.* A basic and asynchronous RTSP client.* The original purpose of this class was to implement a small RTSP client compatible with Wowza.* It implements Digest Access Authentication according to RFC 2069. */
public class RtspClient {public final static String TAG = "RtspClient";/** Message sent when the connection to the RTSP server failed. */public final static int ERROR_CONNECTION_FAILED = 0x01;/** Message sent when the credentials are wrong. */public final static int ERROR_WRONG_CREDENTIALS = 0x03;/** * Message sent when the connection with the RTSP server has been lost for * some reason (for example, the user is going under a bridge).* When the connection with the server is lost, the client will automatically try to* reconnect as long as {@link #stopStream()} is not called. **/public final static int ERROR_CONNECTION_LOST = 0x04;/*** Message sent when the connection with the RTSP server has been reestablished.* When the connection with the server is lost, the client will automatically try to* reconnect as long as {@link #stopStream()} is not called.*/public final static int MESSAGE_CONNECTION_RECOVERED = 0x05;private final static int STATE_STARTED = 0x00;private final static int STATE_STARTING = 0x01;private final static int STATE_STOPPING = 0x02;private final static int STATE_STOPPED = 0x03;private int mState = 0;private int mPort = 0;private class Parameters {public String host; public String username;public String password;public String path;public Session session;public int port;public Parameters clone() {Parameters params = new Parameters();params.host = host;params.username = username;params.password = password;params.path = path;params.session = session;params.port = port;return params;}}private Parameters mTmpParameters;private Parameters mParameters;private Socket mSocket;private String mSessionID;private String mAuthorization;private BufferedReader mBufferedReader;private OutputStream mOutputStream;private int mCSeq;private Callback mCallback;private Handler mMainHandler;private Handler mHandler;/*** The callback interface you need to implement to know what's going on with the * RTSP server (for example your Wowza Media Server).*/public interface Callback {public void onRtspUpdate(int message, Exception exception);}public RtspClient() {mCSeq = 0;mTmpParameters = new Parameters();mTmpParameters.port = 1935;mTmpParameters.path = "/";setCredentials("1102", "123456");mAuthorization = null;mCallback = null;mMainHandler = new Handler(Looper.getMainLooper());mState = STATE_STOPPED;final Semaphore signal = new Semaphore(0);new HandlerThread("net.majorkernelpanic.streaming.RtspClient"){@Overrideprotected void onLooperPrepared() {mHandler = new Handler();signal.release();}}.start();signal.acquireUninterruptibly();}/*** Sets the callback interface that will be called on status updates of the connection* with the RTSP server.* @param cb The implementation of the {@link Callback} interface*/public void setCallback(Callback cb) {mCallback = cb;}/*** The {@link Session} that will be used to stream to the server.* If not called before {@link #startStream()}, a it will be created.*/public void setSession(Session session) {mTmpParameters.session = session;}public Session getSession() {return mTmpParameters.session;} /*** Sets the destination address of the RTSP server.* @param host The destination address* @param port The destination port*/public void setServerAddress(String host, int port) {mTmpParameters.port = port;mTmpParameters.host = host;}/*** If authentication is enabled on the server, you need to call this with a valid username/password pair.* Only implements Digest Access Authentication according to RFC 2069.* @param username The username* @param password The password*/public void setCredentials(String username, String password) {mTmpParameters.username = username;mTmpParameters.password = password;}/*** The path to which the stream will be sent to. * @param path The path*/public void setStreamPath(String path) {mTmpParameters.path = path;}public boolean isStreaming() {return mState==STATE_STARTED|mState==STATE_STARTING;}/*** Connects to the RTSP server to publish the stream, and the effectively starts streaming.* You need to call {@link #setServerAddress(String, int)} and optionnally {@link #setSession(Session)} * and {@link #setCredentials(String, String)} before calling this.* Should be called of the main thread !*/public void startStream(int port) {mPort = port;if (mTmpParameters.host == null) throw new IllegalStateException("setServerAddress(String,int) has not been called !");if (mTmpParameters.session == null) throw new IllegalStateException("setSession() has not been called !");mHandler.post(new Runnable () {@Overridepublic void run() {if (mState != STATE_STOPPED) return;mState = STATE_STARTING;Log.i(TAG,"Connecting to RTSP server...");// If the user calls some methods to configure the client, it won't modify its behavior until the stream is restartedmParameters = mTmpParameters.clone();mParameters.session.setDestination(mTmpParameters.host);try {mParameters.session.syncConfigure();} catch (Exception e) {mParameters.session = null;mState = STATE_STOPPED;return;}             try {tryConnection();} catch (Exception e) {
//                  postError(ERROR_CONNECTION_FAILED);Log.i(TAG,"Exception failed:"+e.getMessage());
//                  abord();return;}try {mParameters.session.syncStart();mState = STATE_STARTED;//                 mHandler.post(mConnectionMonitor);} catch (Exception e) {Log.i(TAG,"ii Exception failed:"+e.getMessage());
//                  abord();}//notify user success.postMessage(0); }});}public void switchCamera(){Stream stream = mParameters.session.getTrack(1);//videoif (stream instanceof VideoStream){VideoStream videoStream = (VideoStream)stream;try {videoStream.switchCamera();} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}public void setFlash(boolean bFlag){Stream stream = mParameters.session.getTrack(1);//videoif (stream instanceof VideoStream){VideoStream videoStream = (VideoStream)stream;try {videoStream.setFlashState(bFlag);} catch (Exception e) {// TODO Auto-generated catch blocke.printStackTrace();}}}/*** Stops the stream, and informs the RTSP server.*/public void stopStream() {mHandler.post(new Runnable () {@Overridepublic void run() {if (mParameters != null && mParameters.session != null) {mParameters.session.stop();}if (mState != STATE_STOPPED) {mState = STATE_STOPPING;abord();}}});}public void release() {stopStream();mHandler.getLooper().quit();}private void abord() {Log.i(TAG, "abord");try {sendRequestTeardown();} catch (Exception ignore) {}try {mSocket.close();} catch (Exception ignore) {}mHandler.removeCallbacks(mConnectionMonitor);mHandler.removeCallbacks(mRetryConnection);mState = STATE_STOPPED; }private void tryConnection() throws IOException {mCSeq = 0;mSocket = new Socket(mParameters.host, mParameters.port);mBufferedReader = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));mOutputStream = mSocket.getOutputStream();sendRequestOption();sendRequestAnnounce();if (sendRequestSetup()){if (!sendRequestRecord()){Log.e(TAG, "Record failed.");postError(ERROR_CONNECTION_FAILED);}}else{postError(ERROR_CONNECTION_FAILED);}}/*** Forges and sends the ANNOUNCE request */private void sendRequestAnnounce() throws IllegalStateException, SocketException, IOException {String body = mParameters.session.getSessionDescription();String request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +"Content-Type: application/sdp\r\n" +"CSeq: " + (++mCSeq) + "\r\n" +"User-Agent: " + "XdjaClient" + "\r\n" +"Content-Length: " + body.length() + "\r\n\r\n" +body; Log.i(TAG,request.substring(0, request.indexOf("\r\n")));mOutputStream.write(request.getBytes("UTF-8"));Response response = Response.parseResponse(mBufferedReader);if (response.headers.containsKey("server")) {Log.i(TAG,"RTSP server name:" + response.headers.get("server"));} else {Log.i(TAG,"RTSP server name unknown");}
//
//      try {
//          Matcher m = Response.rexegSession.matcher(response.headers.get("session"));
//          m.find();
//          mSessionID = m.group(1);
//      } catch (Exception e) {
//          throw new IOException("Invalid response from server. Session id: "+mSessionID);
//      }if (response.status == 401) {String nonce, realm;Matcher m;if (mParameters.username == null || mParameters.password == null) throw new IllegalStateException("Authentication is enabled and setCredentials(String,String) was not called !");try {m = Response.rexegAuthenticate.matcher(response.headers.get("www-authenticate")); m.find();nonce = m.group(2);realm = m.group(1);} catch (Exception e) {throw new IOException("Invalid response from server");}String uri = "rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path;String hash1 = computeMd5Hash(mParameters.username+":"+m.group(1)+":"+mParameters.password);String hash2 = computeMd5Hash("ANNOUNCE"+":"+uri);String hash3 = computeMd5Hash(hash1+":"+m.group(2)+":"+hash2);mAuthorization = "Digest username=\""+mParameters.username+"\",realm=\""+realm+"\",nonce=\""+nonce+"\",uri=\""+uri+"\",response=\""+hash3+"\"\r\n";request = "ANNOUNCE rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +"CSeq: " + (++mCSeq) + "\r\n" +"Content-Type: application/sdp"+ "\r\n" +"Content-Length: " + body.length() + "\r\n" +"Authorization: " + mAuthorization +"Session: " + mSessionID + "\r\n" +"User-Agent: " + "xdja-xa" + "\r\n\r\n" +body+ "\r\n\r\n";Log.i(TAG,request);mOutputStream.write(request.getBytes("UTF-8"));response = Response.parseResponse(mBufferedReader);if (response.status == 401) throw new RuntimeException("Bad credentials !");} else if (response.status == 403) {throw new RuntimeException("Access forbidden !");}}/*** Forges and sends the SETUP request */private boolean sendRequestSetup() throws IllegalStateException, SocketException, IOException {boolean bHaveAudio = false;for (int i=0;i<2;i++) {Stream stream = mParameters.session.getTrack(i);if (stream != null) {if (i == 0){bHaveAudio = true;}/* String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/trackID="+i+" RTSP/1.0\r\n" +"Transport: RTP/AVP/UDP;unicast;client_port="+(5000+2*i)+"-"+(5000+2*i+1)+";mode=record\r\n" +addHeaders();*/int trackId = i;if (!bHaveAudio){trackId = 0;}String request = "SETUP rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+"/streamid="+trackId+" RTSP/1.0\r\n" +"Transport: RTP/AVP/UDP;unicast;client_port="+(mPort+2*i)+"-"+(mPort+2*i+1)+";mode=record\r\n" +addHeaders();Log.i(TAG,request.substring(0, request.indexOf("\r\n")));mOutputStream.write(request.getBytes("UTF-8"));Response response = Response.parseResponse(mBufferedReader);//if (i == 0){try { mSessionID = response.headers.get("session").trim();Log.i(TAG,"mSessionID: "+ mSessionID+ "response.status:"+response.status);} catch (Exception e) {throw new IOException("Invalid response from server. Session id: "+mSessionID);}}if (response.status != 200){Log.i(TAG,"return for resp :" +response.status);return false;}Matcher m;try {if (response.headers.get("transport") == null){Log.i(TAG,"return for not transport");return false;}m = Response.rexegTransport.matcher(response.headers.get("transport")); m.find();stream.setDestinationPorts(Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)));
//                  mParameters.session.syncStart(i);Log.i(TAG, "Setting destination ports: "+Integer.parseInt(m.group(3))+", "+Integer.parseInt(m.group(4)));} catch (Exception e) {e.printStackTrace();int[] ports = stream.getDestinationPorts();Log.i(TAG,"Server did not specify ports, using default ports: "+ports[0]+"-"+ports[1]);}} }return true;}/*** Forges and sends the RECORD request */private boolean sendRequestRecord() throws IllegalStateException, SocketException, IOException {String request = "RECORD rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" +"Range: npt=0.000-\r\n" +addHeaders();Log.i(TAG,request.substring(0, request.indexOf("\r\n")));mOutputStream.write(request.getBytes("UTF-8"));Response resp = Response.parseResponse(mBufferedReader);if (resp.status != 200){return false;}return true;}/*** Forges and sends the TEARDOWN request */private void sendRequestTeardown() throws IOException {String request = "TEARDOWN rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders();Log.i(TAG,request.substring(0, request.indexOf("\r\n")));mOutputStream.write(request.getBytes("UTF-8"));}/*** Forges and sends the OPTIONS request */private void sendRequestOption() throws IOException {String request = "OPTIONS rtsp://"+mParameters.host+":"+mParameters.port+mParameters.path+" RTSP/1.0\r\n" + addHeaders();Log.i(TAG,request.substring(0, request.indexOf("\r\n")));mOutputStream.write(request.getBytes("UTF-8"));Response.parseResponse(mBufferedReader);}    private String addHeaders() {return "CSeq: " + (++mCSeq) + "\r\n" +"Content-Length: 0\r\n" +(mSessionID != null ? "Session: " + mSessionID + "\r\n" :"") +"User-Agent: " + "xdja-xa" + "\r\n" +(mAuthorization != null ? "Authorization: " + mAuthorization +  "\r\n\r\n":"\r\n");}  /*** If the connection with the RTSP server is lost, we try to reconnect to it as* long as {@link #stopStream()} is not called.*/private Runnable mConnectionMonitor = new Runnable() {@Overridepublic void run() {if (mState == STATE_STARTED) {try {// We poll the RTSP server with OPTION requestssendRequestOption();mHandler.postDelayed(mConnectionMonitor, 6000);} catch (IOException e) {// Happens if the OPTION request failspostMessage(ERROR_CONNECTION_LOST);Log.e(TAG, "Connection lost with the server...");mParameters.session.stop();mHandler.post(mRetryConnection);}}}};/** Here, we try to reconnect to the RTSP. */private Runnable mRetryConnection = new Runnable() {@Overridepublic void run() {if (mState == STATE_STARTED) {try {Log.e(TAG, "Trying to reconnect...");tryConnection();try {mParameters.session.start();mHandler.post(mConnectionMonitor);postMessage(MESSAGE_CONNECTION_RECOVERED);} catch (Exception e) {abord();}} catch (IOException e) {mHandler.postDelayed(mRetryConnection,1000);}}}};final protected static char[] hexArray = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'};private static String bytesToHex(byte[] bytes) {char[] hexChars = new char[bytes.length * 2];int v;for ( int j = 0; j < bytes.length; j++ ) {v = bytes[j] & 0xFF;hexChars[j * 2] = hexArray[v >>> 4];hexChars[j * 2 + 1] = hexArray[v & 0x0F];}return new String(hexChars);}/** Needed for the Digest Access Authentication. */private String computeMd5Hash(String buffer) {MessageDigest md;try {md = MessageDigest.getInstance("MD5");return bytesToHex(md.digest(buffer.getBytes("UTF-8")));} catch (NoSuchAlgorithmException ignore) {} catch (UnsupportedEncodingException e) {}return "";}private void postMessage(final int message) {mMainHandler.post(new Runnable() {@Overridepublic void run() {if (mCallback != null) {mCallback.onRtspUpdate(message, null); }}});}private void postError(final int message) {mMainHandler.post(new Runnable() {@Overridepublic void run() {if (mCallback != null) {mCallback.onRtspUpdate(message, null); }}});} static class Response {// Parses method & uripublic static final Pattern regexStatus = Pattern.compile("RTSP/\\d.\\d (\\d+) (\\w+)",Pattern.CASE_INSENSITIVE);// Parses a request headerpublic static final Pattern rexegHeader = Pattern.compile("(\\S+):(.+)",Pattern.CASE_INSENSITIVE);// Parses a WWW-Authenticate headerpublic static final Pattern rexegAuthenticate = Pattern.compile("realm=\"(.+)\",\\s+nonce=\"(\\w+)\"",Pattern.CASE_INSENSITIVE);// Parses a Session headerpublic static final Pattern rexegSession = Pattern.compile("(\\d+)",Pattern.CASE_INSENSITIVE);// Parses a Transport headerpublic static final Pattern rexegTransport = Pattern.compile("client_port=(\\d+)-(\\d+).+server_port=(\\d+)-(\\d+)",Pattern.CASE_INSENSITIVE);public int status;public HashMap<String,String> headers = new HashMap<String,String>();/** Parse the method, uri & headers of a RTSP request */public static Response parseResponse(BufferedReader input) throws IOException, IllegalStateException, SocketException {Response response = new Response();String line;Matcher matcher;// Parsing request method & uriif ((line = input.readLine())==null) throw new SocketException("Connection lost");matcher = regexStatus.matcher(line);matcher.find();response.status = Integer.parseInt(matcher.group(1));// Parsing headers of the requestwhile ( (line = input.readLine()) != null) {//Log.e(TAG,"l: "+line.length()+"c: "+line);if (line.length()>3) {matcher = rexegHeader.matcher(line);matcher.find();Log.i(TAG, "response.headers: "+matcher.group(1).toLowerCase(Locale.US)+": "+ matcher.group(2));response.headers.put(matcher.group(1).toLowerCase(Locale.US), matcher.group(2));} else {break;}}if (line==null) throw new SocketException("Connection lost");Log.i(TAG, "Response from server: "+response.status);return response;}}}

界面部分只需要实例化一个RtspPushStreamClient对象,并调用startRtspClient方法就可以启动推流动作,如果是视频流,则需要添加一个net.majorkernelpanic.streaming.gl.SurfaceView 的SurfaceView,并通过SessionBuilder.getInstance().setSurfaceView设置到spydroid提供的接口中。

在Spydroid-ipcamera基础上做推流的实现之二相关推荐

  1. 如何用直播摄像机、编码器、电脑端OBS软件在抖音平台上做推流直播现实背景

    现实背景 自2021年以来,抖音平台提高了电脑推流直播门槛,导致很多人无法通过电脑软件做推流直播,也无法通过第三方推流工具做直播,具体要求如下图所示: 抖音平台的新规则如下: 硬性条件: 1. 需实名 ...

  2. 网站建设想要出类拔萃还要从基础上做创新

    如今的互联网,随便在网上搜索一下就有许多家网站公司愿意为你制作网站,可是网站千千万,哪个更吸引人呢?那么在当下的新媒体时代用户的浏览量要如何获得呢?下面就为大家进行详细的分析,希望可以帮助到大家更好地 ...

  3. android自定义滚轴选择器_Android自定义滚动式时间选择器(在他人基础上修改)...

    尽管Android给我们提供了时间选择控件DatePicker和TimePicker(它们的使用方法可以参考我的这篇文章Android之日期时间选择控件DatePicker和TimePicker),但 ...

  4. 在没有任何前端开发经验的基础上, 创建第一个 SAP Fiori Elements 应用

    这是 Jerry 2021 年的第 26 篇文章,也是汪子熙公众号总共第 297 篇原创文章. 本文绝非标题党. Jerry 前一篇文章 SAP Cloud Application Programmi ...

  5. 在工作流引擎基础上搭建电子商务揽收系统解决方案

    1.       项目背景 目前使用的揽收系统是2000年针对上门揽收特快专递邮件和演出票而开发的系统,该系统能够实现的功能是把下单信息派单到各处理局,但因下单界面单一,近年来由于业务的增加,表单信息 ...

  6. Android开源项目大合集(转载的基础上添加了项目地址)

    WeChat高仿微信 项目地址:https://github.com/motianhuo/wechat 高仿微信,实现功能有: 好友之间文字聊天,表情,视频通话,语音,语音电话,发送文件等. 知乎专栏 ...

  7. 在水果忍者游戏上做改编的中秋切月饼canvas小游戏

    <中秋切月饼游戏>                                                                                     ...

  8. 分别用Comparable和Comparator两个接口对下列四位同学的成绩做降序排序,如果成绩一样, 那在成绩排序的基础上按照年龄由小到大排序。 姓名(String

    代码 import java.util.*;/*3.分别用Comparable和Comparator两个接口对下列四位同学的成绩做降序排序,如果成绩一样,那在成绩排序的基础上按照年龄由小到大排序.姓名 ...

  9. php能干哪些副业,做副业,在能干的基础上踏实肯干

    原标题:做副业,在能干的基础上踏实肯干 我想,刚刚开始做副业的你,一定胸怀大志,一定想成为一匹被人赏识.驰骋沙场的千里马吧?那么,就好好沉淀下来.低就一层不等于低人一等,今日的俯低是为了明天的高就.所 ...

最新文章

  1. problem h: 一年中的第几天_一年级语文26个汉语拼音字母表读法+写法+笔顺,给孩子收藏!...
  2. linux怎么远程windows桌面,Windows系统怎么远程登陆桌面Linux?
  3. ABC 189 E - Rotate and Flip 矩阵转移
  4. 《Python Cookbook 3rd》笔记(1.2):拆分任意长可迭代对象后赋值给多个变量
  5. SelectSort 选择排序
  6. [包计划] cheerio
  7. 机器学习基础(三十四)—— 协同过滤(之获得推荐)
  8. Realsense安装使用过程问题汇总
  9. servlet到底是什么?
  10. 数据分析师都有哪些发展方向?
  11. jquery fadein css同时用,如何同时运行jQuery fadeIn()和slideDown()?
  12. 安森美推出新的高功率图腾柱PFC控制器,满足具挑战的能效标准
  13. logback 多实例 归档问题 无法自动删除.tmp文件问题
  14. 微信公众平台js算法逆向
  15. Access to script at ‘file:///D:/html/vue-%E6%A8%A1%E5%9D%97%E5%8C%96%E5%BC%80%E5%8F%91/js/aaa.js‘ fr
  16. openGL曲面细分
  17. windows下枚举串口的方法,超好用,跟设备管理器枚举一样
  18. vue商城第13 订单确认模块 14订单成功页面
  19. 后端自学——使用PuTTY远程连接阿里云轻量应用服务器
  20. 生产环境 遭遇 fastJson1.2.78 巨坑

热门文章

  1. 1词法分析PaddleNLP / examples / lexical_analysis
  2. 直立代码分析__两轮平衡小车原理
  3. auto.js之界面ui
  4. hihocoder 1272 买零食
  5. EMI器件原理及应用
  6. 【小白学java】D35》》》线程入门学习,线程(多线程)的实现
  7. 华中科技大学计算机科学与技术学院郑强教授,华中科技大学教授声讨后勤被处分 校方:通报批评,取消2年评优...
  8. 电桥-20151208
  9. OpenCV+Mediapipe手势动作捕捉与Unity引擎的结合
  10. 同样协调个事情,为什么有人一说就通,有人一说就炸?(转,知乎)