今天的主題要來寫關於在Android手機上實現一個以UDP/TCP技術實現在區域網路內實現聊天的技術
而重點我想就如同標題麻...就是TCP/UDP技術
那何謂TCP/UDP技術呢?
首先雖然我把兩個寫在一起,但是他們是不一樣的東西哦( ・ὢ・ )
然後本部落格有寫關於TCP/UDP技術的相關範例都在這裡了
->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊上篇-UDP伺服器端與客戶端實現
->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊中篇-TCP伺服器端實現
->碼農日常-『Android studio』在Android上實現TCP/UDP區域網路通訊下篇-TCP客戶端實現
歡迎參考抄襲โ๏∀๏ใ
雖然就結果來看根本是一樣的
TCP是英文Transmission Control Protocol的縮寫
繁中翻譯傳輸控制協定
UDP則是英文User Datagram Protocol的縮寫
繁中翻譯使用者資料報協定
然後至於他們的相關原理...維基都有我懶得講了XDDDDDD
相關參考這篇好了,這篇講得頗詳細的
->https://ithelp.ithome.com.tw/articles/10205476
反正針對這兩個東西我只做一個結論
UDP傳輸速度快,但容易掉包
TCP傳輸慢,但是傳輸內容可靠穩定
就這樣,還想知道什麼三次握手四次揮手的自己去找文章โ๏∀๏ใ
好啦,那回到我們的重點
今天我下的標題是"在Android上實現TCP/UDP區域網路通訊(上)"
我打算這個系列分成三篇文章來寫
而這篇的主要內容是以UDP技術實現兩台Android手機之間的資料傳輸
之後的兩篇分別是TCP的客戶端與TCP的Server端在Android APP上的實現
那麼,來看一下今天的功能吧
Emmmm...畫面有點模糊
不過應該還看得到在做什麼啦
總之,右邊的APP就是我們在本篇文章要寫的APP
右邊的則是我在Google Play載下來別人寫的APP
在此->https://play.google.com/store/apps/details?id=com.sandersoft.udpmonitor&hl=zh_TW&gl=US
最後,這邊是本次APP的Github
->https://github.com/thumbb13555/UDP_TCP_ConnectDemo/tree/master
開始吧(・ωー)~☆
1. 介紹&加入權限
1.1 介紹專案結構
本次專案為了節省我Github的空間,所以我直接把UDP跟TCP的功能都放在一個APP
怎麼樣?很有誠意對吧(-‿◦)
來看一下專案結構吧
身為一個二合一的APP,所以主畫面(MainActivity.java)便是選擇UDP或是TCP的頁面而已
當然,這次的文章我們是選擇UDP得這個畫面
而在檔案中還有一個CommendFun.java
裡面只放了一個用來取得手機所連線到之WIfi之IP而已
我直接PO一下吧
public class CommendFun { @SuppressLint("DefaultLocale") public static String getLocalIP(Context context) { WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(WIFI_SERVICE); assert wifiManager != null; WifiInfo info = wifiManager.getConnectionInfo(); int ipAddress = info.getIpAddress(); return String.format("%d.%d.%d.%d" , ipAddress & 0xff , ipAddress >> 8 & 0xff , ipAddress >> 16 & 0xff , ipAddress >> 24 & 0xff); } }
再重申一次,本篇文章只有要講到UDP那個資料夾內的內容
TCP的內容要到下篇文章囉~
1.2 加入權限
本次的專案因為涉及到網路使用以及Wifi使用
因此必須加入相關權限
請至AndroidManifest.xml的部分
加入以下粉底白字兩行
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.jetec.udp_tcp_connectdemo"> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.INTERNET"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".TCP.TCPActivity"></activity> <activity android:name=".UDP.UDPActivity" /> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
網路的話不用特別問使用者,因此準備工作到這篇囉
接下來畫介面
2. 畫介面
本次介面長這副德性
直接PO,沒什麼好說的
activity_u_d_p.xml
<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".UDP.UDPActivity"> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline2" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.08" /> <TextView android:id="@+id/textView_IP" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="本機IP: " android:textColor="@android:color/background_dark" android:textSize="18sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/guideline2" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <ToggleButton android:id="@+id/toggleButton_ReceiveSwitch" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textOff="開啟監聽" android:textOn="關閉監聽" app:layout_constraintBottom_toTopOf="@+id/guideline6" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@+id/editText_Port" app:layout_constraintTop_toTopOf="@+id/guideline2" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.3" /> <EditText android:id="@+id/editText_ReceiveMessage" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:layout_marginBottom="8dp" android:focusable="false" android:focusableInTouchMode="false" android:gravity="top" android:minLines="20" android:inputType="textMultiLine" app:layout_constraintBottom_toTopOf="@+id/guideline4" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView3" app:layout_constraintVertical_bias="0.0" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.9" /> <TextView android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="收到的消息" android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textStyle="bold" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline3" /> <EditText android:id="@+id/editText_Input" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="8dp" android:ems="10" android:hint="請輸入內容" android:inputType="textPersonName" android:text="Hello! I'm samsung" app:layout_constraintBottom_toBottomOf="@+id/button_Send" app:layout_constraintEnd_toStartOf="@+id/button_clear" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/button_Send" /> <EditText android:id="@+id/editText_Port" android:layout_width="0dp" android:layout_height="0dp" android:ems="10" android:hint="Port" android:inputType="number" android:text="8888" app:layout_constraintBottom_toBottomOf="@+id/toggleButton_ReceiveSwitch" app:layout_constraintEnd_toStartOf="@+id/guideline5" app:layout_constraintStart_toEndOf="@+id/textView4" app:layout_constraintTop_toTopOf="@+id/toggleButton_ReceiveSwitch" /> <Button android:id="@+id/button_Send" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="發送" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline4" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" app:layout_constraintGuide_percent="0.7" /> <EditText android:id="@+id/editText_RemoteIp" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="8dp" android:ems="10" android:inputType="numberDecimal" android:text="192.168.50.5" app:layout_constraintBottom_toTopOf="@+id/guideline3" app:layout_constraintEnd_toStartOf="@+id/guideline5" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView5" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/guideline6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" app:layout_constraintGuide_percent="0.17" /> <TextView android:id="@+id/textView4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:text="本機Port: " android:textAppearance="@style/TextAppearance.AppCompat.Large" android:textSize="18sp" android:textStyle="bold" app:layout_constraintBottom_toTopOf="@+id/guideline6" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline2" /> <TextView android:id="@+id/textView5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="8dp" android:layout_marginTop="8dp" android:text="Remote Ip:" android:textAppearance="@style/TextAppearance.AppCompat.Body1" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@+id/guideline6" /> <TextView android:id="@+id/textView6" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="Remote Port: " android:textAppearance="@style/TextAppearance.AppCompat.Body1" app:layout_constraintStart_toStartOf="@+id/guideline5" app:layout_constraintTop_toTopOf="@+id/guideline6" /> <EditText android:id="@+id/editText_RemotePort" android:layout_width="0dp" android:layout_height="0dp" android:layout_marginStart="8dp" android:ems="10" android:inputType="textPersonName" android:text="8888" app:layout_constraintBottom_toTopOf="@+id/guideline3" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/guideline5" app:layout_constraintTop_toBottomOf="@+id/textView6" /> <Button android:id="@+id/button_clear" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="清除" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@+id/button_Send" app:layout_constraintTop_toTopOf="@+id/guideline4" /> </androidx.constraintlayout.widget.ConstraintLayout>
原則上直接複製即可( ゚Д゚)b
3. 撰寫UDP主要功能
接下來要撰寫的是UDP功能的主要部分,也就是UDP.java這個檔案
基本上我也是維持我一慣的風格,你可以直接把以下內容全部複製打包帶走(・ωー)~☆
總之先全部給你,再來一段一段程式分析
class UDP implements Runnable { public static final String TAG = "MyUDP"; public static final String RECEIVE_ACTION = "GetUDPReceive"; public static final String RECEIVE_STRING = "ReceiveString"; public static final String RECEIVE_BYTES = "ReceiveBytes"; private int port = 8888; private String ServerIp; private boolean isOpen; private static DatagramSocket ds = null; private Context context; /**切換伺服器監聽狀態*/ public void changeServerStatus(boolean isOpen) { this.isOpen = isOpen; if (!isOpen) { ds.close(); Log.e(TAG, "UDP-Server已關閉"); } } //切換Port public void setPort(int port){ this.port = port; } /**初始化建構子*/ public UDP(String ServerIp,Context context) { this.context = context; this.ServerIp = ServerIp; this.isOpen = true; } /**發送訊息*/ public void send(String string, String remoteIp, int remotePort) throws IOException { Log.d(TAG, "客户端IP:" + remoteIp + ":" + remotePort); InetAddress inetAddress = InetAddress.getByName(remoteIp); DatagramSocket datagramSocket = new DatagramSocket(); DatagramPacket dpSend = new DatagramPacket(string.getBytes(), string.getBytes().length, inetAddress, remotePort); datagramSocket.send(dpSend); } /**監聽執行緒*/ @Override public void run() { /**在本機上開啟Server監聽*/ InetSocketAddress inetSocketAddress = new InetSocketAddress(ServerIp, port); try { ds = new DatagramSocket(inetSocketAddress); Log.e(TAG, "UDP-Server已啟動"); } catch (SocketException e) { Log.e(TAG, "啟動失敗,原因: " + e.getMessage()); e.printStackTrace(); } //預備一組byteArray來放入回傳得到的值(PS.回傳為格式為byte[],本範例將值轉為字串了) byte[] msgRcv = new byte[1024]; DatagramPacket dpRcv = new DatagramPacket(msgRcv, msgRcv.length); //建立while迴圈持續監聽來訪的數值 while (isOpen) { Log.e(TAG, "UDP-Server監聽資訊中.."); try { //執行緒將會在此打住等待有值出現 ds.receive(dpRcv); String string = new String(dpRcv.getData(), dpRcv.getOffset(), dpRcv.getLength()); Log.d(TAG, "UDP-Server收到資料: " + string); /**以Intent的方式建立廣播,將得到的值傳至主要Activity*/ Intent intent = new Intent(); intent.setAction(RECEIVE_ACTION); intent.putExtra(RECEIVE_STRING,string); intent.putExtra(RECEIVE_BYTES, dpRcv.getData()); context.sendBroadcast(intent); } catch (IOException e) { e.printStackTrace(); } } } }
Okay來吧
首先,這整個Class都使用了Runnable方法
Runnable方法基本是跟著執行緒一起使用的
不懂執行緒的話歡迎參考我這篇
->碼農日常-『Android studio』Android背景執行之Thread、Handler及AsyncTask的基礎用法
也就是說,這個Class內run的部分在背景執行緒跑的,不能在這邊寫更動介面的程式噢!
先講這裡提供的方法
三個方法如下
/**切換伺服器監聽狀態*/ public void changeServerStatus(boolean isOpen) { this.isOpen = isOpen; if (!isOpen) { ds.close(); Log.e(TAG, "UDP-Server已關閉"); } } //切換Port public void setPort(int port){ this.port = port; } /**發送訊息*/ public void send(String string, String remoteIp, int remotePort) throws IOException { Log.d(TAG, "客户端IP:" + remoteIp + ":" + remotePort); InetAddress inetAddress = InetAddress.getByName(remoteIp); DatagramSocket datagramSocket = new DatagramSocket(); DatagramPacket dpSend = new DatagramPacket(string.getBytes(), string.getBytes().length, inetAddress, remotePort); datagramSocket.send(dpSend); }
關於關閉Server監聽的部分,我把它寫在changeServerStatus()內了
所以如果傳入為false時,就會把監聽關掉
再來是初始化建構子,在這
/**初始化建構子*/ public UDP(String ServerIp,Context context) { this.context = context; this.ServerIp = ServerIp; this.isOpen = true; }
原則上主要要拿到這支手機連現在到IP,還有Context(上下文)
還有就是打開監聽
最後是RUN的內容
/**監聽執行緒*/ @Override public void run() { /**在本機上開啟Server監聽*/ InetSocketAddress inetSocketAddress = new InetSocketAddress(ServerIp, port); try { ds = new DatagramSocket(inetSocketAddress); Log.e(TAG, "UDP-Server已啟動"); } catch (SocketException e) { Log.e(TAG, "啟動失敗,原因: " + e.getMessage()); e.printStackTrace(); } //預備一組byteArray來放入回傳得到的值(PS.回傳為格式為byte[],本範例將值轉為字串了) byte[] msgRcv = new byte[1024]; DatagramPacket dpRcv = new DatagramPacket(msgRcv, msgRcv.length); //建立while迴圈持續監聽來訪的數值 while (isOpen) { Log.e(TAG, "UDP-Server監聽資訊中.."); try { //執行緒將會在此打住等待有值出現 ds.receive(dpRcv); String string = new String(dpRcv.getData(), dpRcv.getOffset(), dpRcv.getLength()); Log.d(TAG, "UDP-Server收到資料: " + string); /**以Intent的方式建立廣播,將得到的值傳至主要Activity*/ Intent intent = new Intent(); intent.setAction(RECEIVE_ACTION); intent.putExtra(RECEIVE_STRING,string); intent.putExtra(RECEIVE_BYTES, dpRcv.getData()); context.sendBroadcast(intent); } catch (IOException e) { e.printStackTrace(); } } }
基本上大致分成三段
第一段為啟動Server的部分
/**在本機上開啟Server監聽*/ InetSocketAddress inetSocketAddress = new InetSocketAddress(ServerIp, port); try { ds = new DatagramSocket(inetSocketAddress); Log.e(TAG, "UDP-Server已啟動"); } catch (SocketException e) { Log.e(TAG, "啟動失敗,原因: " + e.getMessage()); e.printStackTrace(); }
第二段則為接收資料的部分
//預備一組byteArray來放入回傳得到的值(PS.回傳為格式為byte[],本範例將值轉為字串了) byte[] msgRcv = new byte[1024]; DatagramPacket dpRcv = new DatagramPacket(msgRcv, msgRcv.length); //建立while迴圈持續監聽來訪的數值 while (isOpen) { Log.e(TAG, "UDP-Server監聽資訊中.."); try { //執行緒將會在此打住等待有值出現 ds.receive(dpRcv); String string = new String(dpRcv.getData(), dpRcv.getOffset(), dpRcv.getLength()); Log.d(TAG, "UDP-Server收到資料: " + string); . . . } catch (IOException e) { e.printStackTrace(); } }
最後,就是把資料傳出去了
while (isOpen) { Log.e(TAG, "UDP-Server監聽資訊中.."); try { . . . /**以Intent的方式建立廣播,將得到的值傳至主要Activity*/ Intent intent = new Intent(); intent.setAction(RECEIVE_ACTION); intent.putExtra(RECEIVE_STRING,string); intent.putExtra(RECEIVE_BYTES, dpRcv.getData()); context.sendBroadcast(intent); } catch (IOException e) { e.printStackTrace(); } }
以上就是全部重點程式,接下來就是使用這塊"模組"了
4. 撰寫主要程式
接下來的是對我來說最難解釋的部分( ´_ゝ`)
這次的內容的重點是UDP傳輸技術本身
但是問題是這項技術要成為一個完整的APP的話必須要有很多UI控制項
而這些UI又會干擾要講解的內容本身...
好吧,我盡力講
總之,還好程式不算長,已經盡力縮短了
所以一樣全PO,再來標記號
public class UDPActivity extends AppCompatActivity { EditText edRemoteIp, edLocalPort, edReceiveMessage, edInputBox, edRemotePort; MyBroadcast myBroadcast = new MyBroadcast(); StringBuffer stringBuffer = new StringBuffer(); ExecutorService exec = Executors.newCachedThreadPool(); UDP udpServer; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_u_d_p); //設置基礎UI setBaseUI(); //設置監UDP功能 setReceiveSwitch(); //設置發送資料功能 setSendFunction(); //註冊廣播器,使回傳能夠從其他類別內傳回此Activity IntentFilter intentFilter = new IntentFilter(UDP.RECEIVE_ACTION); registerReceiver(myBroadcast, intentFilter); } private void setBaseUI() { TextView tvLocalIp = findViewById(R.id.textView_IP); //注意:此處有調用CommendFun.java的內容以取得本機IP tvLocalIp.setText("本機IP: " + getLocalIP(this)); //清除所有儲存過的資訊 Button btClear = findViewById(R.id.button_clear); btClear.setOnClickListener(v -> { stringBuffer.delete(0,stringBuffer.length()); edReceiveMessage.setText(stringBuffer); }); edRemoteIp = findViewById(R.id.editText_RemoteIp); edRemotePort = findViewById(R.id.editText_RemotePort); edLocalPort = findViewById(R.id.editText_Port); edReceiveMessage = findViewById(R.id.editText_ReceiveMessage); edInputBox = findViewById(R.id.editText_Input); } private void setReceiveSwitch() { ToggleButton btSwitch = findViewById(R.id.toggleButton_ReceiveSwitch); //初始化UDP伺服器 //注意:此處有調用CommendFun.java的內容以取得本機IP udpServer = new UDP(getLocalIP(this),this); btSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { if (isChecked) { /**開啟UDP伺服器監聽*/ int port = Integer.parseInt(edLocalPort.getText().toString()); udpServer.setPort(port); udpServer.changeServerStatus(true); exec.execute(udpServer); edLocalPort.setEnabled(false); } else { /**關閉UDP伺服器監聽*/ udpServer.changeServerStatus(false); edLocalPort.setEnabled(true); } }); } private void setSendFunction() { Button btSend = findViewById(R.id.button_Send); /**發送UDP訊息至指定的IP*/ btSend.setOnClickListener(v -> { String msg = edInputBox.getText().toString(); String remoteIp = edRemoteIp.getText().toString(); int port = Integer.parseInt(edRemotePort.getText().toString()); if (msg.length() == 0) return; stringBuffer.append("發送: ").append(msg).append("\n"); edReceiveMessage.setText(stringBuffer); //調用UDP.java中的方法,送出資料 //本範例用lambda表達式,原貌在下面註解 exec.execute(()->{ try { udpServer.send(msg, remoteIp, port); } catch (IOException e) { e.printStackTrace(); } }); // exec.execute(new Runnable() { // @Override // public void run() { // try { // udpServer.send(msg, remoteIp, port); // } catch (IOException e) { // e.printStackTrace(); // } // } // }); }); } private class MyBroadcast extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String mAction = intent.getAction(); assert mAction != null; switch (mAction) { /**接收來自UDP回傳之訊息*/ case UDP.RECEIVE_ACTION: String msg = intent.getStringExtra(UDP.RECEIVE_STRING); byte[] bytes = intent.getByteArrayExtra(UDP.RECEIVE_BYTES); stringBuffer.append("收到: ").append(msg).append("\n"); edReceiveMessage.setText(stringBuffer); break; } } } @Override protected void onDestroy() { super.onDestroy(); //取消監聽廣播 unregisterReceiver(myBroadcast); } }
OK,總共有四個顏色的記號
分別是
藍底白字:接收回傳廣播的部分
粉底白字: 開啟Server
綠底白字: 關閉Server
紫底白字: 送出資料
而在本篇,我使程式進入到背景執行緒的方法是用這個
ExecutorService exec = Executors.newCachedThreadPool();
然後在搭配這樣使用
exec.execute(udpServer);
基本上,雖然好像沒頭沒尾
不過需要注意的部分就是如此
整個過程就是
1. 寫好模組
2. 用
就這樣(´◑ω◐`)
但是大家的盲點其實就是不會用模組,或寫不出來而已
所以我標注完模組用在哪邊後,我想應該就相對容易完成了
其他的部分就都是跟UI比較相關的,強烈建議載下我的Github研究研究囉
喔對了~如果有幫助記得幫我點Github星星外加底下拍手五下謝謝XDDDD
今天是2021的1/2號XD
我之前在要放假的時候我就想著既然難得放了連假就來幹一票大的(咦?←難度比較高的意思..
這篇文章本身不會打很久,不過我寫那隻範例程式卻把我的1/1號給吃掉了(゜ロ゜)
程式本身是不會難,可是問題是要怎麼把這麼多東西塞進一個畫面就很傷腦筋
不過好處是今天寫完後下週跟下下週我就不用煩惱要寫什麼了啦顆顆顆
2020去年是一個全世界都過得不太好的一年
不過對我洨洨攻城屍來說我頂多就是因為公司業績差TM去年沒領到中秋獎金跟端午獎金就是了
其實還蠻幹的!!( ゚Д゚)凸
哎算了,反正都過了
祈禱今年獎金可以稍微多一點點吧..
總之雖然這篇文章看著看著是很有壓力的一篇文章
不過還是祝各位新年快樂,寫出很牛逼的程式發大財
如果可以幫我在下面多多拍手,就可以幫碼農我發大財囉ɷ◡ɷ