Obrolan di iOS: menggunakan soket


Gambar dibuat oleh rawpixel.com

Dalam publikasi ini, kita akan turun ke lapisan TCP, belajar tentang soket dan alat-alat dari Core Foundation menggunakan contoh mengembangkan aplikasi obrolan.

Perkiraan waktu membaca: 25 menit.

Mengapa soket?


Anda mungkin bertanya-tanya: "Mengapa saya harus pergi satu tingkat lebih rendah dari URLSession ?" Jika Anda cukup pintar dan tidak mengajukan pertanyaan ini, langsung ke bagian selanjutnya.

Jawabannya untuk tidak begitu pintar
Pertanyaan bagus! Faktanya adalah bahwa penggunaan URLSession didasarkan pada protokol HTTP , yaitu, komunikasi terjadi dalam gaya permintaan-respons , kira-kira sebagai berikut:

  • meminta dari server beberapa data dalam format JSON
  • dapatkan data ini, proses, tampilan, dll.

Tetapi bagaimana jika kita membutuhkan server atas inisiatifnya sendiri untuk mentransfer data ke aplikasi Anda? Di sini HTTP tidak berfungsi.

Tentu saja, kami dapat terus-menerus menarik server dan melihat apakah ada data untuk kami (alias polling ). Atau kita bisa lebih canggih dan menggunakan polling panjang . Tetapi semua kruk ini sedikit tidak pantas dalam kasus ini.

Lagi pula, mengapa membatasi diri pada paradigma permintaan-respons jika itu sesuai dengan tugas kita sedikit kurang dari tidak sama sekali?

Dalam panduan ini, Anda akan belajar cara menyelam ke tingkat abstraksi yang lebih rendah dan langsung menggunakan SOCKETS dalam aplikasi obrolan.

Alih-alih memeriksa pesan baru dari server, aplikasi kami akan menggunakan stream yang tetap terbuka selama sesi obrolan.

Memulai


Unduh bahan sumbernya . Ada aplikasi klien tiruan dan server sederhana yang ditulis dalam Go .

Anda tidak harus menulis di Go, tetapi Anda harus menjalankan aplikasi server agar aplikasi klien dapat terhubung dengannya.

Luncurkan aplikasi server


Bahan sumber memiliki aplikasi terkompilasi dan sumber. Jika Anda memiliki paranoia yang sehat dan tidak mempercayai kode kompilasi orang lain, Anda dapat mengkompilasi sendiri kode sumbernya.

Jika Anda berani, lalu buka Terminal , buka direktori dengan materi yang diunduh dan jalankan perintah:

sudo ./server

Saat diminta, masukkan kata sandi Anda. Setelah itu Anda akan melihat pesan

Mendengarkan pada 127.0.0.1:80.

Catatan: aplikasi server dimulai dalam mode istimewa (perintah "sudo") karena ia mendengarkan pada port 80. Semua port dengan angka kurang dari 1024 memerlukan akses khusus.

Server obrolan Anda sudah siap! Anda dapat pergi ke bagian selanjutnya.

Jika Anda ingin mengkompilasi sendiri kode sumber server,
maka dalam hal ini Anda perlu menginstal Go menggunakan Homebrew .

Jika Anda tidak memiliki Homebrew, maka Anda harus menginstalnya terlebih dahulu. Buka Terminal dan rekatkan baris berikut di sana:

/usr/bin/ruby -e \
"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"


Kemudian gunakan perintah ini untuk menginstal Go:

brew install go

Pada akhirnya, buka direktori dengan bahan sumber yang diunduh dan kompilasi kode sumber aplikasi server:

go build server.go

Akhirnya, Anda dapat memulai server dengan perintah di awal bagian ini .

Kami melihat apa yang kami miliki di klien


Sekarang buka proyek DogeChat , kompilasi dan lihat apa yang ada di sana.



Seperti yang Anda lihat, DogeChat sekarang memungkinkan Anda untuk memasukkan nama pengguna dan pergi ke bagian obrolan itu sendiri.

Tampaknya pengembang proyek ini tidak tahu cara membuat obrolan. Jadi yang kita miliki hanyalah UI dasar dan navigasi. Kami akan menulis lapisan jaringan. Hore!

Buat ruang obrolan


Untuk langsung menuju pengembangan, buka ChatRoomViewController.swift . Ini adalah pengontrol tampilan yang dapat menerima teks yang dimasukkan pengguna dan menampilkan pesan yang diterima dalam tampilan tabel.

Karena kita memiliki ChatRoomViewController , masuk akal untuk mengembangkan kelas ChatRoom yang akan melakukan semua pekerjaan kasar.

Mari kita pikirkan tentang apa yang akan diberikan oleh kelas baru:

  • membuka koneksi ke aplikasi server;
  • menghubungkan pengguna dengan nama yang ditentukan olehnya ke obrolan;
  • mengirim dan menerima pesan;
  • menutup koneksi di akhir.

Sekarang kita tahu apa yang kita inginkan dari kelas ini, tekan Command-N , pilih Swift File dan sebut itu ChatRoom .

Membuat Streaming I / O


Ganti konten ChatRoom.swift dengan ini:

 import UIKit class ChatRoom: NSObject { //1 var inputStream: InputStream! var outputStream: OutputStream! //2 var username = "" //3 let maxReadLength = 4096 } 

Di sini kita mendefinisikan kelas ChatRoom dan mendeklarasikan properti yang kita butuhkan.

  1. Pertama kita mendefinisikan aliran input / output. Menggunakannya sebagai pasangan akan memungkinkan kita untuk membuat koneksi soket antara aplikasi dan server obrolan. Tentu saja, kami akan mengirim pesan menggunakan aliran output, dan menerima menggunakan aliran input.
  2. Selanjutnya, kami mendefinisikan nama pengguna.
  3. Dan akhirnya, kita mendefinisikan variabel maxReadLength, yang membatasi panjang maksimum satu pesan.

Sekarang buka file ChatRoomViewController.swift dan tambahkan baris ini ke daftar propertinya:

 let chatRoom = ChatRoom() 

Sekarang kita telah membuat struktur dasar kelas, sekarang saatnya untuk melakukan tugas-tugas pertama yang direncanakan: membuka koneksi antara aplikasi dan server.

Koneksi terbuka


Kami kembali ke ChatRoom.swift dan menambahkan metode ini untuk definisi properti:

 func setupNetworkCommunication() { // 1 var readStream: Unmanaged<CFReadStream>? var writeStream: Unmanaged<CFWriteStream>? // 2 CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, "localhost" as CFString, 80, &readStream, &writeStream) } 

Inilah yang kami lakukan di sini:

  1. pertama-tama kita mendefinisikan dua variabel untuk aliran socket tanpa menggunakan manajemen memori otomatis
  2. lalu kita, menggunakan variabel yang sama ini, membuat langsung stream yang terikat ke nomor host dan port.

Fungsi ini memiliki empat argumen. Yang pertama adalah jenis pengalokasi memori yang akan kita gunakan saat menginisialisasi utas. Anda harus menggunakan kCFAllocatorDefault , meskipun ada opsi lain yang mungkin jika Anda ingin mengubah perilaku utas.

Catatan Penerjemah
Dokumentasi untuk fungsi CFStreamCreatePairWithSocketToHost mengatakan: gunakan NULL atau kCFAllocatorDefault . Dan deskripsi kCFAllocatorDefault mengatakan bahwa ini adalah sinonim untuk NULL . Lingkaran ditutup!

Lalu kita atur nama host. Dalam kasus kami, kami terhubung ke server lokal. Jika server Anda berada di tempat lain, maka Anda dapat mengatur alamat IP-nya.

Kemudian nomor port yang didengarkan server.

Akhirnya, kami meneruskan pointer ke stream I / O kami sehingga fungsinya dapat menginisialisasi mereka dan menghubungkannya ke stream yang dibuatnya.

Sekarang setelah kita memiliki aliran yang diinisialisasi, kita dapat menyimpan tautan ke sana dengan menambahkan baris-baris ini di akhir metode setupNetworkCommunication () :

 inputStream = readStream!.takeRetainedValue() outputStream = writeStream!.takeRetainedValue() 

Menggunakan takeRetainedValue () sebagaimana diterapkan pada objek yang tidak dikelola memungkinkan kami untuk mempertahankan referensi untuk itu dan, pada saat yang sama, menghindari kebocoran memori di masa depan. Sekarang kita dapat menggunakan utas kami di mana pun kami inginkan.

Sekarang kita perlu menambahkan utas ini ke run loop sehingga aplikasi kita memproses acara jaringan dengan benar. Untuk melakukan ini, tambahkan dua baris ini di akhir setupNetworkCommunication () :

 inputStream.schedule(in: .current, forMode: .common) outputStream.schedule(in: .current, forMode: .common) 

Akhirnya saatnya berlayar! Untuk memulai, tambahkan ini di bagian paling akhir metode setupNetworkCommunication () :

 inputStream.open() outputStream.open() 

Sekarang kami memiliki koneksi terbuka antara aplikasi klien dan server kami.

Kami dapat mengkompilasi dan menjalankan aplikasi kami, tetapi Anda tidak akan melihat perubahan apa pun, karena sementara kami tidak melakukan apa-apa dengan koneksi client-server kami.

Terhubung ke obrolan


Sekarang setelah kita memiliki koneksi yang mapan dengan server, saatnya untuk mulai melakukan sesuatu tentang itu! Dalam hal obrolan, Anda harus memperkenalkan diri terlebih dahulu, dan kemudian Anda dapat mengirim pesan ke lawan bicara.

Ini membawa kita pada kesimpulan penting: karena kita memiliki dua jenis pesan, kita perlu entah bagaimana membedakannya.

Protokol obrolan


Salah satu keuntungan menggunakan lapisan TCP adalah kita dapat mendefinisikan "protokol" kita sendiri untuk komunikasi.

Jika kita menggunakan HTTP, maka kita perlu menggunakan kata-kata berbeda ini GET , PUT , PATCH . Kita perlu membentuk URL dan menggunakan tajuk yang tepat dan semua itu.

Kami hanya memiliki dua jenis pesan. Kami akan kirim

iam:Luke

untuk memasuki obrolan dan memperkenalkan diri Anda.

Dan kami akan kirim

msg:Hey, how goes it, man?

untuk mengirim pesan obrolan kepada semua responden.

Ini sangat sederhana, tetapi benar-benar tidak berprinsip, jadi jangan gunakan metode ini dalam proyek-proyek penting.

Sekarang kita tahu apa yang diharapkan server kami dan kami dapat menulis metode di kelas ChatRoom yang akan memungkinkan pengguna untuk terhubung ke obrolan. Satu-satunya argumen adalah nama panggilan pengguna.

Tambahkan metode ini di dalam ChatRoom.swift :

 func joinChat(username: String) { //1 let data = "iam:\(username)".data(using: .utf8)! //2 self.username = username //3 _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } //4 outputStream.write(pointer, maxLength: data.count) } } 

  1. Pertama kita membentuk pesan kita menggunakan "protokol" kita sendiri
  2. Simpan nama untuk referensi di masa mendatang.
  3. withUnsafeBytes (_ :) menyediakan cara mudah untuk bekerja dengan pointer yang tidak aman di dalam penutupan.
  4. Akhirnya, kami mengirim pesan kami ke arus keluaran. Ini mungkin terlihat lebih rumit daripada yang Anda harapkan, namun tulis (_: maxLength :) menggunakan pointer tidak aman yang dibuat pada langkah sebelumnya.

Sekarang metode kami siap, buka ChatRoomViewController.swift dan tambahkan panggilan ke metode ini di akhir viewWillAppear (_ :) .

 chatRoom.joinChat(username: username) 

Sekarang kompilasi dan jalankan aplikasi. Masukkan nama panggilan Anda dan ketuk kembali untuk melihat ...



... itu lagi tidak ada yang berubah!

Tunggu, tidak apa-apa! Pergi ke jendela terminal. Di sana Anda akan melihat pesan bahwa Vasya telah bergabung atau sesuatu seperti itu jika nama Anda bukan Vasya.

Ini hebat, tetapi alangkah baiknya memiliki indikasi koneksi yang sukses di layar ponsel Anda.

Menanggapi Pesan Masuk


Server mengirimkan klien yang bergabung dengan pesan ke semua orang yang ada dalam obrolan, termasuk Anda. Untungnya, aplikasi kita sudah memiliki segalanya untuk menampilkan pesan yang masuk dalam bentuk sel dalam tabel pesan di ChatRoomViewController .

Yang harus Anda lakukan adalah menggunakan inputStream untuk β€œmenangkap” pesan-pesan ini, mengonversinya menjadi instance dari kelas pesan , dan meneruskannya ke meja untuk ditampilkan.

Agar dapat menanggapi pesan yang masuk, Anda perlu ChatRoom untuk mematuhi protokol StreamDelegate .

Untuk melakukan ini, tambahkan ekstensi ini di bagian bawah file ChatRoom.swift :

 extension ChatRoom: StreamDelegate { } 

Sekarang nyatakan siapa yang akan menjadi delegasi untuk inputStream.

Tambahkan baris ini ke metode setupNetworkCommunication () tepat sebelum panggilan untuk menjadwalkan (di: forMode :):

 inputStream.delegate = self 

Sekarang tambahkan stream (_: handle :) implementasi metode ke ekstensi:

 func stream(_ aStream: Stream, handle eventCode: Stream.Event) { switch eventCode { case .hasBytesAvailable: print("new message received") case .endEncountered: print("The end of the stream has been reached.") case .errorOccurred: print("error occurred") case .hasSpaceAvailable: print("has space available") default: print("some other event...") } } 

Kami memproses pesan masuk


Jadi, kami siap untuk mulai memproses pesan masuk. Acara yang menarik minat kami adalah .hasBytesAvailable , yang menunjukkan bahwa pesan masuk telah tiba.

Kami akan menulis metode yang memproses pesan-pesan ini. Di bawah metode yang baru ditambahkan, kami menulis yang berikut:

 private func readAvailableBytes(stream: InputStream) { //1 let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: maxReadLength) //2 while stream.hasBytesAvailable { //3 let numberOfBytesRead = inputStream.read(buffer, maxLength: maxReadLength) //4 if numberOfBytesRead < 0, let error = stream.streamError { print(error) break } // Construct the Message object } } 

  1. Kami mengatur buffer di mana kami akan membaca byte yang masuk.
  2. Kami berputar dalam satu lingkaran, sementara di aliran input ada sesuatu untuk dibaca.
  3. Kami menyebutnya read (_: maxLength :), yang membaca byte dari stream dan menempatkannya di buffer.
  4. Jika panggilan mengembalikan nilai negatif, kami mengembalikan kesalahan dan keluar dari loop.

Kita perlu memanggil metode ini segera setelah kita memiliki data di aliran masuk, jadi pergi ke pernyataan beralih di dalam aliran (_: menangani :) metode, cari saklar .hasBytesAvailable dan panggil metode ini segera setelah pernyataan cetak:

 readAvailableBytes(stream: aStream as! InputStream) 

Di tempat ini kami menyiapkan penyangga data yang diterima!

Tetapi kita masih perlu mengubah buffer ini menjadi isi dari tabel pesan.

Tempatkan metode ini di readAvailableBytes (stream :) .

 private func processedMessageString(buffer: UnsafeMutablePointer<UInt8>, length: Int) -> Message? { //1 guard let stringArray = String( bytesNoCopy: buffer, length: length, encoding: .utf8, freeWhenDone: true)?.components(separatedBy: ":"), let name = stringArray.first, let message = stringArray.last else { return nil } //2 let messageSender: MessageSender = (name == self.username) ? .ourself : .someoneElse //3 return Message(message: message, messageSender: messageSender, username: name) } 

Pertama, kita menginisialisasi String menggunakan buffer dan ukuran yang kita berikan ke metode ini.

Teks akan berada di UTF-8, pada akhirnya kita akan membebaskan buffer, dan membagi pesan dengan simbol ':' untuk memisahkan nama pengirim dan pesan itu sendiri.

Sekarang kami sedang menganalisis apakah pesan ini berasal dari peserta lain. Pada produk, Anda dapat membuat sesuatu seperti token unik, ini sudah cukup untuk demo.

Akhirnya, dari semua ekonomi ini, kami membentuk instance dari Pesan dan mengembalikannya.

Untuk menggunakan metode ini, tambahkan if-let di akhir loop sementara di metode readAvailableBytes (stream :) , segera setelah komentar terakhir:

 if let message = processedMessageString(buffer: buffer, length: numberOfBytesRead) { // Notify interested parties } 

Sekarang semuanya siap untuk disampaikan kepada seseorang Pesan ... Tetapi kepada siapa?

Buat Protokol ChatRoomDelegate


Jadi, kami perlu memberi tahu ChatRoomViewController.swift tentang pesan baru tersebut, tetapi kami tidak memiliki tautan ke sana. Karena ini berisi tautan ChatRoom yang kuat, kita bisa jatuh ke dalam perangkap siklus tautan yang kuat.

Ini adalah tempat yang sempurna untuk membuat protokol delegasi. ChatRoom tidak peduli siapa yang perlu tahu tentang posting baru.

Di bagian atas ChatRoom.swift, tambahkan definisi protokol baru:

 protocol ChatRoomDelegate: class { func received(message: Message) } 

Sekarang di dalam kelas ChatRoom, tambahkan tautan yang lemah ke toko yang akan menjadi delegasi:

 weak var delegate: ChatRoomDelegate? 

Sekarang mari kita tambahkan metode readAvailableBytes (stream :) , menambahkan baris berikut di dalam konstruksi if-let, di bawah komentar terakhir dalam metode:

 delegate?.received(message: message) 

Kembali ke ChatRoomViewController.swift dan tambahkan ekstensi kelas berikut, yang memastikan kepatuhan dengan protokol ChatRoomDelegate, segera setelah MessageInputDelegate:

 extension ChatRoomViewController: ChatRoomDelegate { func received(message: Message) { insertNewMessageCell(message) } } 

Proyek asli sudah berisi yang diperlukan, jadi insertNewMessageCell (_ :) akan menerima pesan Anda dan menampilkan sel yang benar di tampilan tabel.

Sekarang tetapkan view controller sebagai delegasi dengan menambahkan ini ke viewWillAppear (_ :) segera setelah memanggil super.viewWillAppear ()

 chatRoom.delegate = self 

Sekarang kompilasi dan jalankan aplikasi. Masukkan nama dan ketuk kembali.



Anda akan melihat sel tentang koneksi Anda ke obrolan. Hore, Anda berhasil mengirim pesan ke server dan menerima tanggapan darinya!

Posting Pesan


Sekarang ChatRoom dapat mengirim dan menerima pesan, sekarang saatnya untuk memberikan kemampuan kepada pengguna untuk mengirim frasa mereka sendiri.

Di ChatRoom.swift, tambahkan metode berikut di akhir definisi kelas:

 func send(message: String) { let data = "msg:\(message)".data(using: .utf8)! _ = data.withUnsafeBytes { guard let pointer = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) else { print("Error joining chat") return } outputStream.write(pointer, maxLength: data.count) } } 

Metode ini mirip dengan joinChat (nama pengguna :) , yang kami tulis sebelumnya, kecuali bahwa ia memiliki awalan msg di depan teks (untuk menunjukkan bahwa ini adalah pesan obrolan nyata).

Karena kami ingin mengirim pesan dengan tombol Kirim , kami kembali ke ChatRoomViewController.swift dan temukan MessageInputDelegate di sana.

Di sini kita melihat metode sendWasTapped (message :) kosong. Untuk mengirim pesan, kirim ke chatRoom:

 chatRoom.send(message: message) 

Sebenarnya, itu saja! Karena server akan menerima pesan dan meneruskannya ke semua orang, ChatRoom akan diberitahu tentang pesan baru dengan cara yang sama seperti ketika bergabung dengan obrolan.

Kompilasi dan jalankan aplikasi.



Jika Anda tidak memiliki orang untuk diajak ngobrol sekarang, luncurkan jendela terminal baru dan masukkan:

nc localhost 80

Ini akan menghubungkan Anda ke server. Sekarang Anda dapat terhubung ke obrolan menggunakan "protokol" yang sama:

iam:gregg

Maka - kirim pesan:

msg:Ay mang, wut's good?



Selamat, Anda menulis klien untuk mengobrol!

Kami membersihkan diri


Jika Anda pernah mengembangkan aplikasi yang secara aktif membaca / menulis file, maka Anda harus tahu bahwa pengembang yang baik menutup file ketika mereka selesai bekerja dengannya. Faktanya adalah bahwa koneksi melalui soket disediakan oleh deskriptor file. Ini berarti bahwa setelah menyelesaikan pekerjaan Anda harus menutupnya, seperti file lainnya.

Untuk melakukan ini, tambahkan metode berikut ke ChatRoom.swift setelah mendefinisikan kirim (pesan :) :

 func stopChatSession() { inputStream.close() outputStream.close() } 

Seperti yang mungkin Anda tebak, metode ini menutup utas sehingga Anda tidak dapat lagi menerima dan mengirim pesan. Selain itu, utas dihapus dari run loop yang sebelumnya kami tempatkan.

Tambahkan panggilan ke metode ini di bagian .endEncountered dari pernyataan sakelar di dalam aliran (_: handle :) :

 stopChatSession() 

Kemudian kembali ke ChatRoomViewController.swift dan lakukan hal yang sama di viewWillDisappear (_ :) :

 chatRoom.stopChatSession() 

Itu saja! Sekarang sudah pasti!

Kesimpulan


Sekarang setelah Anda menguasai dasar-dasar jaringan dengan soket, Anda dapat memperdalam pengetahuan Anda.

Soket UDP


Aplikasi ini adalah contoh dari komunikasi jaringan menggunakan TCP, yang menjamin pengiriman paket ke tujuan.

Namun, Anda dapat menggunakan soket UDP. Jenis koneksi ini tidak menjamin pengiriman paket ke tujuan yang dimaksudkan, tetapi jauh lebih cepat.

Ini sangat berguna dalam gim. Pernah mengalami kelambatan? Ini berarti Anda memiliki koneksi yang buruk dan banyak paket UDP hilang.

Soket web


Alternatif lain untuk HTTP dalam aplikasi adalah teknologi yang disebut soket web.

Tidak seperti soket TCP biasa, soket web menggunakan HTTP untuk membangun komunikasi. Dengan bantuan mereka, Anda dapat mencapai hal yang sama dengan soket biasa, tetapi dengan kenyamanan dan keamanan, seperti di browser.

Source: https://habr.com/ru/post/id467909/


All Articles