Xin chào, lại là mình đây – baka3k – một coder thích nói lan man về những điều nhảm nhí
Đã rất lâu rồi mình ko viết về những thứ ngoài công nghệ, lần này mình sẽ cố gắng giữ sự cân bằng để ko đưa quá nhiều toxic vào bài viết này, nên nếu bạn thấy mình trong đó, đừng giật mình, vì mình không cố ý đâu
4 mức độ hiểu biết(vài chỗ thì người ta sẽ gọi là 4 mức độ ngu dốt) gồm 4 level sau:
Không Biết rằng mình Không Biết
Không Biết rằng mình Biết
Biết rằng mình Không Biết
Biết rằng mình Biết
Không Biết rằng mình Không Biết
Hay còn gọi là Ngu mà không biết là mình ngu
Cái Ngu ở đây mình đang nói về cái ngu của Trí giả, ko phải cái ngu mang tính miệt thị đâu nhé
Thời kỳ mông muội sơ khai, khi linh trí của tu hành giả chưa được khai mở, ý lộn… khi lập trình viên còn chưa biết mình không biết cái gì, cần học cái gì, cần đi theo đường nào.
Đôi khi nó xuất hiện cả ở những level cao hơn một chút Middle hoặc Senior. Có những giai đoạn các bạn này làm việc & phát triển bản thân trong vô định
Vô định ở đây là các bạn làm rất tốt công việc hiện tại của mình, nhưng loay hoay không biết mình nên phát triển tiếp thế nào vì mọi thứ đều đang rất tốt, các bài toán công ty đưa cho đều ở mức độ vừa phải, không nhiều thách thức. Ừ thì đại khái là training vài bạn fresher, đôi khi review code hộ ai đó, đôi khi fix bug hộ, làm vài cái seminar, hiểu cách triển khai vài cái design pattern…blabla. Mọi thứ các bạn đều làm được, các bạn cảm thấy mình đang đứng ở đỉnh của domain đang làm việc, cuối chuỗi thức ăn. Lúc này rất dễ dàng để bạn lạc lối. rất dễ dàng để tự nhận rank của mình là Expert, Guru. Nếu bạn tự thấy mình đang ở level này, thì nên ngồi xuống, bình tĩnh lại và tự review bản thân mình thật cẩn thận.
Senior 5 năm Kinh nghiệm, Expert 7 năm kinh nghiệm, Guru 10 năm kinh nghiệm… chỉ làm loanh quanh mấy cái app client server, vài ba cái architecture MVVM, MVP, rồi cao hơn tý là Hexagonal hay Clean architecture – đó là một sự giễu cợt
Tuy người ta không thể lấy số năm kinh nghiệm để đánh giá về Competency của một người, nhưng nó sẽ phản ánh đúng được phần nào đó, giống như lái xe ô tô cần khoảng 1 vạn km, lái máy bay cần khoảng 10.000 giờ bay mới có thể tạm gọi là nhuần nhuyễn vậy.
Tất nhiên, còn liên quan đến lĩnh vực công việc bạn đang làm việc, ví dụ như 5 năm CRUD, 5 năm làm ứng dụng client server chả hạn… thì thôi, bỏ đi. Nếu bạn không muốn ngồi trong giếng, hãy nghiêm túc suy nghĩ lại con đường phát triển bản thân, tìm các cột mốc mới, tìm ra những điểm thiếu sót – nếu không thể, hãy tìm cho mình một Mentor có tâm và đủ Tầm
Không Biết rằng mình Biết
Ai cũng sẽ ở giai đoạn này, chúng ta đều sẽ gặp giai đoạn này trong quá trình phát triển bản thân
Khi chúng ta học và khám phá, khi chúng ta dung nhập một lượng kiến thức khổng lồ vào, trước khi biến nó thành của bản thân mình, bạn sẽ thấy keyword này quen quen, mình thấy nó rồi, có thể nó là lĩnh vực này
Đó là chúng ta, khi vào giai đoạn Không biết là mình Biết
Biết rằng mình không biết
Mọi thứ trở lên rõ ràng hơn, kiến thức được sắp xếp lại, được hoạch định đúng cách hơn
Bạn đủ overview, đủ nhận thức để phân biệt giữa biết lờ mờ, biết keyword, nắm vững, hiểu biết chuyên sâu…etc
Đừng khinh thường những cụm từ được bôi đen này vì nó giúp bạn không lạc lối
Việc không hiểu rõ các cụm từ này – sẽ khiến bạn quay về thời kỳ mông muội Không biết là mình Không Biết
Khi bạn biết là mình không biết cái gì, bạn mới có thể có kế hoạch để cải thiện, hoạch định lại bản thân một cách rõ ràng hơn
Đây là thời điểm bạn sẵng sàng và đủ năng lượng để trở lên mạnh mẽ hơn, thời điểm này mang lại sức bật rất lớn cho hầu hết tất cả mọi người. Nếu bạn ở trong thời điểm này, đừng bỏ lỡ, tận dụng tốt, bạn có thể đi rất xa
Trong phần trước chúng ta đã tìm hiểu về MITM attack và một số thủ thuật đi kèm
Trước khi đi vào detail rule/coding tips ở những phần sau, phần thứ 2 này mình muốn chia sẻ thêm về 1 số rule config project, runtime application để hạn chế việc application bị tấn công, sửa đổi.
Hành động này gần như là bắt buộc với tất các ứng dụng sử dụng giao thức HTTPS.
Tuy nhiên trên Google developer page chúng ta có thể thấy một đoạn warning như sau
Caution: Certificate Pinning is not recommended for Android applications due to the high risk of future server configuration changes, such as changing to another Certificate Authority, rendering the application unable to connect to the server without receiving a client software update.
Sự lo lắng này hoàn toàn hợp lý, khi certificate hết hạn, khi thay đổi certificate..etc – không có cách nào chắc chắn người dùng sẽ cập nhật phần mềm, đồng nghĩa với việc người dùng không thể kết nối tới máy chủ, và có thể chúng ta sẽ mất một lượng user rất lớn. Nên nếu sử dụng SSL pinning, chúng ta phải handle được các case liên quan đến certification và phải tính đến khả năng force update đối với client khi có exception liên quan đến ssl certificate
Có rất nhiều cách để triển khai SSL pinning, tuỳ thuộc cách thức mà ứng dụng kết nối với server mà chúng ta lựa chọn item phù hợp
private fun buildCertificatePinner(): CertificatePinner {
return if (pinning != null && pinning.isNotEmpty()) {
val builder = CertificatePinner.Builder()
for (item in pinning) {
builder.add(item.value, item.key)
}
builder.build()
} else {
CertificatePinner.Builder()
.add("api.themoviedb.org", "sha256/+vqZVAzTqUP8BGkfl88yU7SQ3C8J2uNEa55B7RZjEg0=") // your hash key (pinning SSL)
.build()
}
}
}
Sử dụng
val pining = buildCertificatePinner()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(pining)
.build()
Hash Key? – sha256 lấy ở đâu?
Trong 2 ví dụ ở trên chúng ta đều thấy sự xuất hiện của chuỗi ký tự sha256, có nhiều cách để lấy chuỗi này, ví dụ dùng open-ssl hoặc cách đơn giản nhất là truy cập
https://www.ssllabs.com/ – Điền link muốn check vào và thông tin sẽ hiện ra như sau
Đây là một kỹ thuật mình thấy khá hiếm người sử dụng trong ứng dụng(mặc dù nó rất dễ, mất vài dòng code thôi) – dùng để ngăn chặn(phần nào đó) việc APK bị sửa đổi, thêm mã độc.
Hầu hết lập trình viên Android đều từng ít nhất một lần tải APK từ nguồn ko chính thống, ví dụ apkpure, apkresult hoặc các game trên appstorevn
Điều gì đảm bảo các apk này là apk sạch? ko có bị sửa đổi với mục đích thu thập thông tin người dùng, chèn quảng cáo hoặc tệ hơn là Phishing, hoặc tạo ra các attack vector để tấn công vào một hệ thống nào đó – ở đây có thể là hệ thống của chính chúng ta? – nếu đó là APK của chúng ta?
Ví dụ, attacker có thể modify một file APK cho app ngân hàng/ví điện tử nào đó, ở giao diện đăng nhập thay vì việc gửi thông tin đăng nhập thì attacker sẽ log lại thông tin đăng nhập này, bao gồm mật khẩu, password và gửi về máy chủ của attacker chả hạn.
Một file APK có thể được sửa đổi và sign đi sign lại nhiều lần, cách nhanh nhất để kiểm tra một file APK có bị sửa đổi rồi sign lại hay ko là check signature của file APK, signature là duy nhất, nó chỉ được gen ra bởi key dùng để sign apk và ko có cách nào từ signature trong apk sinh ngược được ra sign key cả.
Trên source code ứng dụng, khi vào một màn hình nào đó chúng ta có thể check SHA dựa vào đoạn code đơn giản bên dưới
Signature[] sigs = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
for (Signature sig : sigs)
{
Trace.i("MyApp", "Signature hashcode : " + sig.hashCode());
}
Compare các mã sha này => có thể phán đoán được ứng dụng có bị sửa đổi hay ko, tuỳ thuộc business, bạn có thể quyết định stop hẳn ứng dụng hoặc disable các tính năng tương ứng.
Giả sử chúng ta có 2 API key dùng cho test server và production server thì sao?
object MovieDBServer {
const val MOVIE_DB_ACCESS_KEY_DEBUG = "xxxxxxxxxxxxx"
const val MOVIE_DB_ACCESS_KEY_PRODUCTION = "xxxxxxxxxxxxx"
}
Tất nhiên khi query thì phải check như thế này
@GET("popular")
suspend fun getPopular(
@Query("api_key") clientId: String = if (BuildConfig.DEBUG)
MovieDBServer.MOVIE_DB_ACCESS_KEY_DEBUG
else MovieDBServer.MOVIE_DB_ACCESS_KEY_PRODUCTION
): MovieResult
Có 2 issue ở đây
Có source code là có key, key không được quản lý tập trung
Nếu thêm nhiều loại key, vd stagging, production, develop..etc thì phải sửa source để build
Để giải quyết vấn đề này, người ta sẽ đẩy việc define các key ra file – ngoài source code và sẽ inject vào trong quá trình build time, các job build khác nhau cho các môi trường khác nhau sẽ inject các API key khác nhau. Tức là thế này
@GET("popular")
suspend fun getPopular(
@Query("api_key") clientId: String = BuildConfig.MOVIE_DB_ACCESS_KEY // chú ý dòng này BuildConfig.
): MovieResult
MOVIE_DB_ACCESS_KEY được inject vào tại build time, nên có thể tuỳ chỉnh, thay đổi cho các job build khác nhau.
Một ví dụ đơn giản, mình chọn lưu ra gradle.properties
Tương tự đối với các sensitive information như user/password của private repository, pass của keystore..etc chúng ta cũng có thể inject trong build time như vậy
Gần đây khi mình tham gia các buổi phỏng vấn Senior thì nhận ra một điều, hầu hết các bạn "Senior" đều có note trong CV rằng "review source code" là một phần công việc của bạn ý, có điều khi mình hỏi các bạn review source code như thế nào thì đều chỉ nói được rất chung chung theo kiểu: kiểm tra source code có dễ đọc ko, có đúng logic trong file specs ko..etc, rất hiếm người trả lời một cách bài bản.
Vậy rốt cuộc " Review source code" là task thế nào hoặc chúng ta nên trả lời cái gì khi bị hỏi: review source là review cái gì?
Bạn luôn luôn phải xây dựng một bộ check list, một bộ quan điểm cho dòng dự án mình đang tham gia. Mỗi dòng dự án sẽ có độ ưu tiên khác nhau cho từng mục trong checklist, sẽ có những mục là mandatory với dự án này nhưng là optional với dự án khác.
Check list này phải được training cho dev trong dự án để hiểu từng item một. Điều này hạn chế việc lack quan điểm review, hạn chế các sai lầm trong những lúc mệt mỏi kém tỉnh táo trong ngày làm việc căng thẳng, hạn chế sự xung đột của reviewer với developer.
Mọi quan điểm đúng/sai đều phải dựa trên offical document nào đó.
Nếu là đúng sai về mặt requirement thì phải có requirement chứng minh lỗi sai của developer, nếu là đúng sai về mặt coding, framework thì phải chỉ ra được lỗi quy định trên ngôn ngữ lập trình hoặc official document của framework.
Tuyệt đối ko dựa vào "Kinh nghiệm cá nhân" để áp đặt tư tưởng bản thân xuống team. Thậm chí kể cả coding convention cũng phải có document để refer.
Việc cần làm của chúng ta là đọc hiểu rồi tuân theo. Điều này khiến source code của dự án trở lên "thân thiện" với người đọc hơn, có vẻ chuyên nghiệp hơn. Bạn đã làm quen với các project chuẩn coding convention mà chuyển sang đọc source một bạn sinh viên lần đầu đi làm, hoặc các bạn làm theo kiểu free style sẽ cảm thấy rất khác biệt, đôi khi có follow coding convention hay không lại là cơ sở để đánh giá liệu rằng coder đã code đủ lâu, đã có kinh nghiệm đủ lâu với lĩnh vực mà họ đang làm hay chưa.
2. Design
Quan điểm review liên quan đến design là một quan điểm rất rộng và tốn giấy mực, nó không chỉ đơn thuần việc các module/class làm việc với nhau thế nào, tạo interface ra sao, abstraction ổn chưa..etc mà còn là việc liên quan đến việc thiết kế bao nhiêu luồng chạy cho ứng dụng là đủ, context để các task chạy có đủ lâu, đủ ổn định không, các component đã đủ yêu cầu liên quan đến security chưa… và nhiều thứ phức tạp liên quan nữa
Security: Secure coding, việc tổ chức source code thế nào, trong code lưu các api key ra sao, có bao nhiêu module dữ liệu được đi ra ngoài, cơ chế lưu dữ liệu, mã hoá dữ liệu, cơ chế đảm bảo an toàn cho các component khi bị gọi đến, quan điểm này thật sự dài, có thể mình sẽ viết trong các seri tiếp theo tại đây
(https://magz.techover.io/2021/08/02/giao-thuc-bao-mat-https-va-mitm-attacksecure-coding-p1/)
Performance: Bao nhiêu thread là đủ, có cần pool thread không? đã giải phóng các resource chưa, dùng loại layout này đã tối ưu chưa, dùng loop nào để optimize tốc độ, có cần cache không, dùng cấu trúc dữ liệu nào để optimize đoạn xử lý này? task này chạy trên worker thread hay main thread..etc
Cấu trúc: có cực kỳ nhiều thứ để check, vd: sử dụng pattern nào, có nên dùng DI ko, đoạn source này nên do class/moduel nào xử lý..etc Mình rất dị ứng cụm từ "dựa vào kinh nghiệm" nhưng thực sự quan điểm này phần lớn do đến từ kinh nghiệm, đại khái thì chúng ta sẽ quan tâm đến high cohesion, low coupling, tạo ra các boundaries – các ranh giới cho class/ module của chương trình
3. Mistake liên quan đến Logic code/requirement
Đây chính là phần mà hầu hết các bạn đi phỏng vấn sẽ trả lời vào, đại khái là check xem có đúng requirement ko, code logic có nhầm đoạn nào ko? có null rồi bị bug ko..etc
Rồi,đó là lý thuyết, giờ chúng ta thử áp dụng để review đoạn source bên dưới
public class BroadCastTestJava extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
decodeImage(intent.getStringExtra("imageData"));
new MyAsyncTask("A").execute();
new MyAsyncTask("B").executeOnExecutor();
}
private Bitmap decodeImage(String image) {
byte[] imgBytes = Base64.decode(image, 0);
return BitmapFactory.decodeByteArray(imgBytes, 0, imgBytes.length);
}
public class MyAsyncTask extends AsyncTask {
private String name;
public MyAsyncTask(String name) {
this.name = name;
}
@Override
protected Object doInBackground(Object[] objects) {
for (int i = 0; i < 10; i++) {
Log.d("test", "My name is " + this.name + " " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return null;
}
}
1. Dùng Tool check.
Đa số các IDE đều hỗ trợ, ví dụ android studio thì có thể làm như dưới
Hầu hết các lỗi do tool chỉ ra, đều đúng là lỗi thật và có thể fix được, nên đương nhiên, chúng ta sẽ xử lý nhé 😛
2. Coding convention
Anh em thử xem có các lỗi convention nào :p
3. Design
Có thể thấy có các issue sau:
decodeImage() không được chạy trên main thread (task vụ nặng)
public class phải chuyển thành private static class(Trong java inner class sẽ refer sang outer class -> leak memory)
Không được dùng asynctask trên BroadcastReceiver, context của broadcast không đủ dài để xử lý background, nên phải đưa về context sống lâu hơn, ví dụ service để xử lý tiếp. Đây là vấn đề liên quan đến thiết kế các task chạy ở đâu cho phù hợp. Hoặc nếu task ngắn hạn thì có thể dùng goAsync(https://developer.android.com/reference/android/content/BroadcastReceiver#goAsync())
Security: Liệu rằng intent gửi đến(để ra lệnh cho app decode image) có phải intent của app ko? có đúng là được phép để xử lý không? hay do attacker đang cố tình tấn công vào app? liệu rằng Broadcast có được bảo vệ bởi permission không? – phần này mình sẽ break sang một bài viết khác liên quan đến Secure Coding
4. Logic code
decodeImage không verify intent đầu vào/ không handle exception, giả sử intent nhả ra null hoặc rỗng thì sẽ có exception xảy ra, có thể app sẽ crash??
=> gọi coder ra "nhờ" bạn ý fix thôi
Việc review code easy hơn rồi – đúng ko nào. Chắc sẽ còn nhiều lỗi nữa, anh em thử check tiếp nhé.
Tổng kết lại:
Chúng ta nên có 1 bộ checklist dùng để review(có thể tuỳ vào dòng dự án, mức độ khó tính của khách hàng, mức độ chặt chẽ dự án yêu cầu)- để hạn chế các mistake về mặt con người
Các quan điểm review đúng/sai về mặt kỹ thuật phải thuần tuý dựa vào quan điểm kỹ thuật, official document để quyết định đúng/sai, không được áp đặt dựa vào "kinh nghiệm cá nhân"
Như tất cả lập trình viên đều biết: HTTPS là một giao giức bảo mật, dữ liệu được mã hóa trên đường truyền, các bước bắt tay (handshake) để mã hóa được dữ liệu của nó tóm gọn bởi các bước bên dưới
Các bước handshake này sẽ đảm bảo dữ liệu giữa client và server được mã hóa bởi một key mà chỉ có client và server biết. Sẽ không ai có thể đọc trộm hoặc sửa đổi các gói tin giữa client và server
Tuy nhiên, nhìn sơ đồ trên chúng ta có thể thấy mắt xích yếu nhất của các bước HandShake chính là step 1 và step 2
Về mặt logic: Nếu như ở Step 1 client say hello với "ai đó" ko phải server, Step 2 server response với "ai đó" ko phải là client thì sao? nếu như client và sever ko làm làm việc trực tiếp với nhau mà thông qua "ai đó" thì Masterkey ở step 7 đã bị "ai đó"lấy – và "ai đó" có khả năng tóm được gói tin, có khả năng giải mã được gói tin, có khả năng gói tin sẽ bị sửa đổi trong quá trình khi gửi nhận giữa client và server?
Nếu điều này xảy ra, đó chính là MTTM attack – Man In The Middle Attack – một hình thức tấn công chen vào giữa đường truyền để lấy dữ liệu, sửa đổi, giả mạo gói tin
Hầu hết các kết nối hiện tại của web, mobile đều đang sử dụng HTTPS để gửi nhận dữ liệu trên đường truyền. Điều này khiến đại đa số lập trình viên yên tâm và hài lòng với về mức độ an toàn này. Nếu sử dụng cách chặn gói tin thông thường thì cái mà attacker thu được chỉ là một gói dữ liệu đã mã hóa – không có giá trị gì.
Nhưng thực tế có đúng như thế ko?
Không, tất nhiên là không, hiện thực tàn khốc hơn thế rất nhiều
Trên thực tế, dữ liệu gửi từ A sang B trên Internet ko thực sự đi trực tiếp từ A sang B, mà có thể nó sẽ còn phải qua rất nhiều server Trung gian khác.
Quay lại lý thuyết về các bước HandShake – với hai mắt xích yếu nhất là Step1 và Step2 – Giả sử chúng ta lừa được client(ở đây là mobile) rằng ServerXXX mới là con server, lừa nốt server(ở đây là Server mà chúng ta cần kết nối đến) rằng ServerXXX mới là client thì sao?
Bingo!!!
Tức là thực tế dữ liệu đã gửi đến 1 con ServerXXX trung gian, sau đó forward cho client hoặc server – nghĩa là dữ liệu này hoàn toàn có thể được đọc, được giải mã, được sửa đổi ở server ServerXXX?
Chúng ta có thể kiểm chứng suy luận này một cách dễ dàng theo cách bên dưới.
(Mình xin nhấn mạnh lại một lần rằng, đây chỉ là một trò chơi ở level Kiddy có thể dùng trong việc kiểm thử – còn trên thực tế, sẽ tàn khốc hơn thế này rất nhiều. Nên việc chú trọng vào Security khi thiết kế, lập trình ứng dụng là cực kỳ quan trọng)
MITM attack với WebProxy
Chuẩn bị
Cài Web Proxy trên máy tính: Fiddler (Window), Charles (Mac)
Chuẩn bị 1 con điện thoại Android
Kết nối Máy tính, Android vào cùng 1 giải mạng
Cài đặt proxy của điện thoại Android trỏ vào IP của máy tính: mục đích mọi gói tin đi qua android đều đi qua proxy là máy tính
Mô hình mạng sẽ như sau
Do Mobile nhận PC là proxy, nên toàn bộ việc gửi nhận dữ liệu – nói một cách chính xác là: toàn bộ hoạt động liên quan đến internet của Mobile đều bị Proxy – ở đây là PC giám sát
Khi mở Webproxy trên máy tính – ở đây mình dùng Fiddler – chúng ta có thể nhìn thấy các gói tin
Tất nhiên, các gói tin này nếu dùng HTTPS thì sẽ là gói tin mã hóa – Không có giá trị gì
Vậy làm sao để giải mã các gói tin này? Easy, follow 4 steps bên dưới nhé
1. Setting Proxy của điện thoại trỏ vào PC(Giả sử IP của PC là 192.168.0.100)
2. Setting WebProxy để capture HTTPS Connect & Decrypt data
Vào setting của WebProxy, chọn 2 mục
Capture HTTPS CONNECTs
Decrypt HTTPS traffic
Sang thẻ connection
Port :ở đây mình chọn port 8888 – đây chính là port để setup proxy cho Mobile
Chọn Allow remote computers to connect
3. Export Cert của webproxy và cài vào điện thoại
Export root cert của webproxy sau đó copy vào điện thoại – cài cert như bình thường
Mục đích của việc này chính là để WebProxy và điện thoại có thể "hiểu nhau" – sau hành động mọi kết nối đến internet từ điện thoại – thông qua proxy đã ko còn bí mật – Proxy sẽ nhìn được toàn bộ dữ liệu của điện thoại
4. Kết quả test thử với việc đăng nhập account Fsoft
Dù là giao thức HTTPS nhưng dữ liệu username/password vẫn phơi thân ra như ảnh dưới
Mọi người có thể thử với 1 số ngân hàng, ví điện tử, không phải ngân hàng/ví điện tử nào cũng áp dụng các cơ chế để phòng tránh MITM attack.
Mình đã thử với 1 số ví điện tử/ngân hàng(mà ko tiện kể tên ra) thì thấy dữ liệu dạng này vẫn phơi thân ra mời gọi đầy quyến rũ 😛
Replay gói tin
Một tính năng cực kỳ hay ho của các tool Web Proxy là chúng ta có thể Replay gói tin, thậm chí sửa dữ liệu trước khi replay. Tức là ví dụ Chuyển 10 đồng thì chúng ta có thể sửa lại thành 200 đồng rồi replay gói tin. Tình cờ 1 cách đen đủi bạn code backend ko tính khả năng này là chúng ta đã có 200 đồng rồi.
Đây cũng chính là thủ thuật áp dụng để trick điểm một cách quang minh chính đại trên Server GST – Hero. VD mình chơi được 30 điểm thì mình có thể sửa lại dữ liệu thành 50 điểm rồi đẩy lên server bằng tính năng Replay
Yeah, câu hỏi quan trọng nhất: Phòng chống MITM attack thế nào?
Client(mobile, web..etc) cần làm gì, Server cần làm gì?
Nếu bạn là 1 developer, nếu bạn có nhiều hơn 2 năm kinh nghiệm, nếu lĩnh vực chính của bạn là client server, ví điện tử, financial…etc thì chúng ta sẽ buộc phải quan tâm đến vấn đề này
Mình post bài lấy chỉ tiêu nên chúng ta hẹn nhau ở post sau nhé 😛
Đây thực sự là một câu hỏi không dễ để trả lời…
Một cách thông thường, trong suy nghĩ của hầu hết lập trình viên sẽ là: chỗ nào giống nhau, gọi lại nhiều lần thì nên gom thành common class, static cho dễ gọi.
Ok, mọi thứ đều ổn, nhưng nếu ko để ý & quá tay một chút thì nó đã vi phạm nghiêm trọng đến design ứng dụng – nó phá vỡ ranh giới của các element,module hoặc class
Tại sao lại như vậy?
Bản chất Lập trình OOP là mô phỏng thực tế cuộc sống vào chương trình bằng ngôn ngữ lập trình. Thay vì dùng văn tả cảnh, thì lập trình viên tả lại cuộc sống bằng ngôn ngữ lập trình trên một framework nào đấy. Ở ngoài cuộc sống có gì, thì chương trình cũng có cái đó, chúng ta có Sinh viên, có Tài khoản ngân hàng, có Ô tô…etc.
Nhưng nếu bạn để ý, trong cuộc sống Util class, Helper class, Common class… ko tồn tại.
Ai đó tạo lên cuộc sống hẳn phải là một lập trình viên cực kỳ vĩ đại.
Phân ranh giới. (Boundaries)
Software architect là một nghệ thuật để tạo ra các ranh giới, nhằm phân tách các element, class, module..etc
Cùng xem xét ví dụ sau:
DisplayHelper – static function
ZxingDecoder cần lấy ra orientation device, hàm lấy orientation dùng ở rất nhiều nơi nên tạo sẵn một class DisplayHelper để dùng. Hàm get orientation cần context nên parameter của ZxingDecoder là context
Display – interface
Mình tạo ra 1 interface là Display để có thể get orientation của device, parameter đầu vào của ZxingDecoder là display.
Điểm khác biệt lớn nhất là ở cách viết 2 – ZxingDecoder đã tách khỏi(1 phần tách khỏi) framework android – khi nó xóa đi sự hiện diện của context, tức là mình đang cố gắng phân rõ ranh giới của ZxingDecoder khỏi android framework
Nói một cách khác mình đang cố gắng decouple ZxingDecoder khỏi android framework
ZxingDecoder là 1 bộ decode barcode – nó ko phụ thuộc vào framwork, việc decode này mang ý nghĩa – class mình viết có thể chạy được ở bất cứ đâu, không chỉ là trên android framework
Ngoài ra, nếu sử dụng DisplayHelper – khi có càng nhiều nơi, càng nhiều class, layer, module gọi hàm get orientation, hoặc một function của DisplayHelper thì các bạn có thể tượng tượng rằng DisplayHelper như một sợi xích đâm xuyên tất cả các layer, module, class, và buộc chặt các thành phần này lại với nhau và hoàn toàn không thể tách rời. Nói cách khác, khi đó ko thể tạo ra các ranh giới Boundaries cho bất cứ thành phần nào dùng chung DisplayHelper. Tất cả đều phẳng, và dính chặt vào android framework thông qua context của DisplayHelper
Dễ code(Create) – khác với việc dễ maintain(Update).
Phân ranh giới. (Boundaries) – là target cho hầu hết các task refactor bạn phải làm
Làm thế nào để phân ranh giới? à, cái ý mình ko dám nói 😀 (ở bài viết này)
(Một bộ phim kinh điển mà mình cực kỳ thích nên lấy nó làm title cho bài viết này) Đây là bản “hồi ký” khi mình tìm solution cho một bài toán, hi vọng nó sẽ giúp ai đó định hướng được đường phải đi.
Bài toán: mix 2 file audio trên android mà không dùng thêm library nào
Suy nghĩ đầu tiên là tìm xem android có chìa API nào ra để mix file ko – bỏ đi, các cậu ko cần search, android ko chìa api nào ra để làm việc này đâu
Suy nghĩ thứ 2: Đưa dữ liệu âm thanh(mp3..etc) về dạng raw data và thử mix các bit vào nhau? có vẻ khả thi
The bad:
private byte[] mBufData1 = null;
private byte[] mBufData2 = null;
private ArrayList mBufMixedData = new ArrayList<>();
private void loadMixedData() {
int length1 = mBufData1.length;
int length2 = mBufData2.length;
int max = Math.max(length1, length2);
int tempSplitSize;
if (length1 == length2) {
for (int i = 0; i < length1; i++) {
mBufMixedData.add((byte) (mBufData1[i] + mBufData2[i]));
}
} else {
if (length2 > length1) {
tempSplitSize = length1;
} else {
tempSplitSize = length2;
}
for (int i = 0; i < tempSplitSize; i++) {
mBufMixedData.add((byte) (mBufData1[i] + mBufData2[i]));
}
if (length2 > length1) {
for (int i = tempSplitSize; i < max; i++) {
mBufMixedData.add((mBufData2[i]));
}
} else {
for (int i = tempSplitSize; i < max; i++) {
mBufMixedData.add((mBufData1[i]));
}
}
}
}
Chạy được thật, âm thanh đã được mix lại như file karaoke ngoài hàng. Nhưng nhìn source code thì chỉ có đứa mù dở mới ko nhìn thấy vấn đề OOM chắc chắn phát sinh. Nếu dừng ở đây – Ok, nó chạy được, chúng ta sẽ giấu đi đoạn OOM kia, kệ cho dự án hót shit vì còn lâu họ mới test ra issue ý, lúc phát hiện chúng ta đã cao chạy xa bay rồi. Ka ka ka Chúng ta là những kẻ tồi tệ
The Ugly
Cải tiến hơn 1 chút, để tránh OOM, mình ko load hết dữ liệu lên ram nữa mà đưa vào DataOutputStream để write từng bit xuống file
private void createWaveMixing(String p1, String p2, String p3) throws IOException {
int size1 = 0;
int size2 = 0;
int size1;
int size2;
FileInputStream fis1 = null;
FileInputStream fis2 = null;
try {
fis1 = new FileInputStream(p1);
fis2 = new FileInputStream(p2);
size1 = fis1.available();
size2 = fis2.available();
long totalAudioLen = size1;
if (size1 < size2) {
totalAudioLen = size2;
}
long totalDataLen = totalAudioLen + WavUtil.LENGTH_EXTENDED;
long longSampleRate = WavUtil.getSampleRate(p1);//44100
long totalDataLen = totalAudioLen + WavUtils.LENGTH_EXTENDED;
long longSampleRate = MediaCodecUtils.getSampleRate(p1);//44100
int channels = 2;
long byteRate = WavUtil.RECORDER_BPP * longSampleRate * channels / 8;
long byteRate = WavUtils.RECORDER_BPP * longSampleRate * channels / 8;
DataOutputStream out = null;
try {
out = new DataOutputStream(new FileOutputStream(p3));
WavUtil.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
WavUtils.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
out.write(toByteArray(mBufMixedData));
} catch (Exception e) {
Log.e(TAG, "#createWaveMixing():" + e.getMessage(), e);
} finally {
if (out != null) {
out.close();
}
}
} finally {
if (fis1 != null) {
fis1.close();
}
if (fis2 != null) {
fis2.close();
}
}
}
private byte[] toByteArray(ArrayList in) {
byte[] data = new byte[in.size()];
for (int i = 0; i < data.length; i++) {
data[i] = in.get(i);
}
return data;
}
}
Cách làm này tránh được OOM, nhưng phải nói là nó chậm, thực sự chậm, chậm kinh khủng. Đâu đó mất khoảng 50 -60 s cho 2 file music raw dài 3 phút. Nói chung là chạy được, ko có issue gì cả, chỉ chậm thôi. Chậm thì tự tìm cách mà improve đi, kêu gì – đúng ko các cậu? – lại chả phải quá. Nếu dừng ở đây, chúng ta là những gã lừa đảo.
Nhưng dù sao mình cũng là người vừa đẹp trai lại tốt tính, nên mới xuất hiện tình huống thứ 3
The Good
Lần này mình optimize bằng cách sử dụng FileChannel, thay vì handle từng bit một, thì mình bóc một nhóm lớn ra để mix(buôn sỉ mới nhanh giầu)
private void createWaveMixing(String p1, String p2, String p3) throws IOException {
int size1;
int size2;
FileInputStream fis1 = null;
FileInputStream fis2 = null;
try {
fis1 = new FileInputStream(p1);
fis2 = new FileInputStream(p2);
size1 = fis1.available();
size2 = fis2.available();
long totalAudioLen = size1;
if (size1 < size2) {
totalAudioLen = size2;
}
long totalDataLen = totalAudioLen + WavUtils.LENGTH_EXTENDED;
long longSampleRate = MediaExtractorUtils.getSampleRate(p1);//44100
int channels = 2;
long byteRate = WavUtils.RECORDER_BPP * longSampleRate * channels / 8;
DataOutputStream out = null;
FileChannel fc1 = fis1.getChannel();
FileChannel fc2 = fis2.getChannel();
long length1 = fc1.size();
long length2 = fc2.size();
try {
out = new DataOutputStream(new FileOutputStream(p3));
WavUtils.writeWaveFileHeader(out, totalAudioLen, totalDataLen, longSampleRate, channels, byteRate);
{
ByteBuffer buff1 = ByteBuffer.allocate(BUFFER_SIZE);
ByteBuffer buff2 = ByteBuffer.allocate(BUFFER_SIZE);
ByteBuffer mixedBuffer = null;
if (length1 == length2) {
while (fc1.read(buff1) > 0) {
fc2.read(buff2);
mixedBuffer = mixByteBuffer(buff1, buff2);
out.write(mixedBuffer.array());
buff1.clear();
buff2.clear();
mixedBuffer.clear();
}
if (mixedBuffer != null) {
mixedBuffer.clear();
}
} else {
if (length2 > length1) {
while (fc1.read(buff1) > 0) {
fc2.read(buff2);
mixedBuffer = mixByteBuffer(buff1, buff2);
out.write(mixedBuffer.array());
buff1.clear();
buff2.clear();
mixedBuffer.clear();
}
while (fc2.read(buff2) > 0) {
out.write(buff2.array());
buff2.clear();
}
} else {
while (fc2.read(buff2) > 0) {
fc1.read(buff1);
mixedBuffer = mixByteBuffer(buff1, buff2);
out.write(mixedBuffer.array());
buff1.clear();
buff2.clear();
mixedBuffer.clear();
}
while (fc1.read(buff1) > 0) {
out.write(buff1.array());
buff1.clear();
}
}
}
}
} catch (Exception e) {
Log.e("test", "#createWaveMixing():" + e.getMessage(), e);
} finally {
if (out != null) {
out.close();
}
fc1.close();
fc2.close();
}
} finally {
if (fis1 != null) {
fis1.close();
}
if (fis2 != null) {
fis2.close();
}
}
}
Kết quả tốc độ tăng gấp 6-8 lần mà lại ko có issue gì cả Lần này(dường như) mình sẽ có thể là người tốt
Cùng 1 bài toán, sẽ có các cách giải khác nhau Khoảng thời gian cho phép khác nhau, chúng ta cũng sẽ phải chọn các giải pháp khác nhau(chấp nhận được, chạy có issue, chậm…etc)
1 ngày làm được ko? được – the bad 1 tuần làm được ko? được – the ugly 1 tháng làm được ko? được – the good
Cái gì cũng sẽ có giá của nó, tùy vào deadline, tùy vào thời điểm, chúng ta hãy cố chọn ra được cách làm tốt nhất
1 phút dành cho quảng cáo, GDK đã có 1 phiên bản có thể record, mix, nối audio nhé, anh em có thể search gdk-soundutilities
Điểm mạnh của IJKPlayer là low latency, nó có độ trễ khá thấp khi streaming, nhưng giả sử phát sinh tình huống cần record một đoạn video khi đang streaming thì phải làm thế nào? IJKPlayer không support sẵn.
Sau khi tham khảo 1 số blog của các bạn…..Trung Quốc và build được thành công thì mình share lại thông tin cho ai cần(vâng, ko hiểu tại sao cứ liên quan đến video, camera,Streaming thì tài liệu chỉ có thể tìm thấy bên….Trung Quốc. Không phủ nhận, các bạn ý giỏi thật )
Trước hết, các bạn cần build được IJKPlayer cho Android đã nhé, cụ thể có thể xem link bên dưới
OK, Sau khi các bạn biết cách chuẩn bị môi trường và build thành công, chúng ta cần tiến hành chỉnh sửa một chút trong thư viện IJKplayer(Mình sẽ không đề cập đến vấn đề pháp lý, licence ở đây)
Chúng ta cần check out source code và build thử nhé
git clone https://github.com/baka3k/IjkPlayerRecorder.git
cd IjkPlayerRecorder
// Khởi tạo project build cho Android
./init-android.sh
//IJKPlayer có sử dụng ffmpeg - nên việc build ffmpeg là bắt buộc
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
//quay lại thư mục IJK và build IJKPlayer
cd ..
./compile-ijk.sh all
Nếu thành công – tức là bạn đã setup đầy đủ, có thể build IJKPlayer được, sẵn sàng cho việc thêm code để thêm chức năng recorder cho IJKPlayer.
Tiếp tục nhé
1.Trong thư mục ijkmedia/ijkplayer bạn tìm đến file ff_ffplay.h và khai báo các hàm sau:
Có nhiều cách để play RTSP trên Android, ứng cử viên hàng đầu là VideoView sẵn có trên Android SDK. Tuy nhiên Video View có rất nhiều hạn chế khi chúng ta cần custom lại, ví dụ chỉnh sửa thêm thắt vào protocol, add các hiệu ứng hình ảnh vào video khi đang play, record, chuyển đổi các track..etc. Khi đó chúng ta phải lựa chọn một giải pháp khác, cụ thể ở đây mình đang nói đến
Exoplayer
IJKPlayer
Exoplayer (https://github.com/google/ExoPlayer) open source, apache licence, java core, rất dễ dàng để anh em lập trình android/java tiếp xúc nhưng điểm trừ của nó là độ trễ – low latency
Khi test với 1 số server RTSP, ExoPlayer cho ra độ trễ vào khoảng 1.2s đến 2.5s tùy chất lượng video out put. Còn IJKPlayer(https://github.com/bilibili/ijkplayer) thì có thể đạt 0.6s đến 0.8s. Điểm trừ của IJKPlayer là licence, IJKPlayer sử dụng FFMPEG, một thư viện rất nổi tiếng, nên nếu ko muốn dính dáng đến pháp lý khi bán sản phẩm, bạn nên cẩn thận khi lựa chọn nó cho khách hàng hoặc nhúng vào sản phẩm.
Để build được IJKPlayer cũng khá đơn giản(nếu bạn chuẩn bị đủ môi trường), hướng dẫn này mình viết chạy trên môi trường MAC OS nhé, Window hoặc Ubuntu cách làm tương tự, tuy nhiên môi trường chuẩn bị sẽ khác đi 1 chút
Check out source code
Các bạn có thể lấy source IJK từ nhánh chính ở trên hoặc từ repo bên dưới, repo này mình đã add thêm một vài chỉnh sửa liên quan đến RTSP và Recorder
Cài đặt NDK cho Mac: mình test thực tế thì thấy build trên bản NDK 10, NDK16(khá cũ) thì ok nhất, ko cần sửa lỗi và chỉnh lại config build, tất nhiên các bạn pro hơn có thể thử với version NDK khác
git clone https://github.com/baka3k/IjkPlayerRecorder.git
cd IjkPlayerRecorder
./init-android.sh
cd android/contrib
./compile-ffmpeg.sh clean
./compile-ffmpeg.sh all
cd ..
./compile-ijk.sh all
Sau khi build xong các bạn có thể tìm thấy file .so trong thư mục