Sockets

Introducció

28029

Com ja sabeu, cada ordinador a Internet té una adreça per identificar-lo de manera única i permetre que altres ordinadors hi interaccionin.

Suposem que necessiteu escriure una aplicació de xarxa, com ara una missatgeria instantània o un joc en línia. Per a aquest propòsit, heu d'organitzar la interacció entre diversos usuaris de la vostra aplicació. Podeu implementar aquesta interacció mitjançant sockets.

Què és un socket?

Un sòcol és una interfície per enviar i rebre dades entre diferents processos (programes en execució) en ordre bidireccional. Es determina per la combinació de l'adreça de l'ordinador a la xarxa (per exemple, 127.0.0.1, que és la vostra pròpia màquina) i un port d'aquesta màquina. Un port és un nombre enter de 0a 65535, preferiblement més gran que 1024(p. ex., 8080, 32254, etc.). Per tant, un sòcol pot tenir l'adreça següent: 127.0.0.1:32245.

Per iniciar la comunicació, un programa anomenat client crea un sòcol per sol·licitar una connexió amb un altre programa anomenat servidor . Amb aquesta finalitat, el client utilitza l'adreça de la màquina del servidor i un port específic on el servidor escolta les sol·licituds entrants. El servidor espera noves sol·licituds i les accepta o no. Si s'accepta una sol·licitud, el servidor crea un sòcol per a la interacció amb el client. Tant el client com el servidor poden enviar-se dades entre ells mitjançant els seus sòcols. Per regla general, un servidor interactua amb diversos clients. El temps que dura la interacció depèn de l'aplicació.

Nota: tant el programa client com el servidor poden estar al mateix ordinador o en màquines diferents connectades mitjançant una xarxa.

Utilitzant endolls a Kotlin

Els sòcols a Kotlin s'utilitzen amb l'ús de dues classes principals de Java, totes dues ubicades al java.netpaquet:

La Socketclasse representa un costat d'una connexió bidireccional (utilitzada per clients i servidors).

La ServerSocketclasse representa un tipus especial de sòcol que escolta i accepta connexions als clients (només utilitzat pels servidors).

La imatge següent mostra quan hem d'utilitzar les dues classes.

Socket i ServerSocket

Socket i ServerSocket

Per tant, un programa client utilitza a Socketper enviar una sol·licitud de connexió. El servidor ha d' ServerSocketacceptar la sol·licitud i després en crea una nova Socketper a la interacció amb el client.

Com a exemple, escriurem un petit servidor d'eco , que rep missatges dels clients i després els envia de tornada. Com a resultat, tindrem dos programes: un client i un servidor. Cada programa té la seva pròpia mainfunció.

El codi del costat del servidor

Primer, considerem el codi del servidor. Per crear un sòcol de servidor, utilitzem la següent instrucció:

val server: ServerSocket = ServerSocket(34522)

L' serverobjecte escolta al port 34522les peticions de connexió dels clients.

El servidor pot acceptar un client nou i crear un sòcol per interactuar amb ell:

val socket: Socket = server.accept() // a socket to interact with a new client

El acceptmètode obliga el programa a esperar un nou client, és a dir, s'executa fins que arribi un nou client. Després d'aquesta declaració, tenim un objecte socket que es pot utilitzar per interactuar amb el client.

Per enviar i rebre dades, necessitem fluxos d'entrada i sortida. Per llegir dades d'una font, s'utilitza un flux d'entrada . A més, les dades s'escriuen a una destinació mitjançant un flux de sortida. Per exemple, el flux d'entrada estàndard obté dades del teclat, mentre que el flux de sortida estàndard imprimeix dades a una pantalla. Per tal de comunicar dades a través de sockets, s'han d'utilitzar les classes DataInputStreami . DataOutputStreamAquests es troben al java.iopaquet.

val input = DataInputStream(socket.getInputStream()) val output = DataOutputStream(socket.getOutputStream())

La invocació input.readUTF()rep un missatge de cadena del client.

La invocació output.writeUTF(message)envia un missatge de cadena al client.

Si necessiteu enviar o rebre alguna cosa com ara pel·lícules o fitxers d'àudio, podeu treballar directament amb bytes en lloc de cadenes.

A continuació, podeu veure un programa de servidor complet que accepta clients en bucle i els torna a enviar missatges.

import java.io.* import java.net.*

const val PORT = 34522

fun main(args: Array) { try { ServerSocket(PORT).use { server -> while (true) { server.accept().use { socket -> DataInputStream(socket.getInputStream()).use { input -> DataOutputStream(socket.getOutputStream()).use { output -> val msg = input.readUTF() // read a message from the client output.writeUTF(msg) // resend it to the client } } } } } } catch (e: IOException) { e.printStackTrace() } }

Com podeu veure, aquest codi demostra clarament les idees bàsiques de la comunicació socket. El programa només en crea una ServerSocketi després accepta connexions de client en un bucle infinit. El programa s'atura quan es produeix un tancament.

Atenció: gestionem les excepcions de manera simplificada. També utilitzem el usemètode d'extensió perquè el sòcol i els fluxos es puguin tancar automàticament per evitar fuites de recursos.

El codi del costat del client

Per començar a interactuar amb un servidor, necessitem almenys un client.

En primer lloc, hauríem de crear un sòcol, especificant el camí i el port d'un servidor.

val socket = Socket("127.0.0.1", 23456) // address and port of a server

Aquí, utilitzem l' adreça localhost ( "127.0.0.1") com a exemple. En un cas real, el vostre servidor estarà allotjat en un altre ordinador. Per tant, és una bona pràctica prendre l'adreça i el port d'una configuració externa o d'arguments de línia d'ordres.

Per enviar i rebre dades al servidor, necessitem fluxos d'entrada i sortida:

val input = DataInputStream(socket.getInputStream()) val output = DataOutputStream(socket.getOutputStream())

Abans hem considerat com utilitzar-los.

A continuació es mostra un programa client complet que es connecta a un servidor, envia un missatge i imprimeix el missatge rebut des del servidor.

import java.io.* import java.net.*

const val SERVER_ADDRESS = "127.0.0.1" const val SERVER_PORT = 34522

fun main(args: Array) { try { Socket(SERVER_ADDRESS, SERVER_PORT).use { socket -> DataInputStream(socket.getInputStream()).use { input -> DataOutputStream(socket.getOutputStream()).use { output -> val msg = readln() output.writeUTF(msg) // send a message to the server val receivedMsg = input.readUTF() // read the reply from the server println("Received from the server: $receivedMsg") } } } } catch (e: IOException) { e.printStackTrace() } }

Com abans, utilitzem el usemètode d'extensió per tancar el sòcol i els fluxos.

Per demostrar el nostre programa client, primer hem d'iniciar el servidor i després executar un o més programes client.

Hello! Received from the server: Hello!

Un altre exemple:

What? Received from the server: What?

El símbol >no forma part de l'entrada. Només marca la línia d'entrada.

Donant servei a diversos clients connectats durant molt de temps Si volem desenvolupar un xat o un servidor de jocs, els nostres clients no s'aturaran després d'enviar un sol missatge. Periòdicament enviaran i rebran missatges al/des del servidor.

Intentem millorar el nostre client per enviar cinc missatges al servidor d'eco. Aquí hi ha un codi de client modificat que s'ha de col·locar dins de la trydeclaració:

for (i in 0..4) { val msg = readln() output.writeUTF(msg) val receivedMsg = input.readUTF() println(receivedMsg) }

El servidor també es va modificar per acceptar els cinc missatges d'un client.

for (i in 0..4) { val msg = input.readUTF() // read the next client message output.writeUTF(msg) // resend it to the client }

Funcionarà perfectament per interactuar amb un sol client:

Hello1 Received from the server: Hello1 Hello2 Received from the server: Hello2 Hello3 Received from the server: Hello3 Hello4 Received from the server: Hello4 Hello5 Received from the server: Hello5

Però si comencem dos o més clients, notarem un efecte estrany. El servidor no interactuarà amb el segon client abans de respondre a tots els missatges del primer client. Com és que? Perquè utilitzem només un fil per processar missatges de tots els clients.

Servidor multifil

La manera més senzilla de treballar amb diversos clients simultàniament és utilitzar multithreading! Deixeu que un fil del servidor (per exemple, main) accepti clients nous mentre que altres interactuen amb els clients ja acceptats (un fil per client).

Aquí teniu el nostre servidor modificat amb una abstracció nova anomenada Session. Com que cada sessió funciona en un fil independent, podem executar diverses sessions simultàniament.

import java.io.* import java.net.*

const val PORT = 34522

fun main(args: Array) { try { ServerSocket(PORT).use { server -> while (true) { val session = Session(server.accept()) session.start() // does not block this server thread println(session) } } } catch (e: IOException) { e.printStackTrace() } }

class Session(private val socket: Socket) : Thread() { override fun run() { try { DataInputStream(socket.getInputStream()).use { input -> DataOutputStream(socket.getOutputStream()).use { output -> for (i in 0..4) { val msg = input.readUTF() output.writeUTF(msg) } socket.close() } } } catch (e: IOException) { e.printStackTrace() } } }

Si iniciem aquesta versió del servidor i diversos clients, tots els clients podran rebre missatges.

És important tenir en compte que no utilitzem el usemètode d'extensió perquè server.accept()creem un sòcol en un fil i el tanquem en un altre. En aquest cas, seria molt propens a errors, ja que un fil podria tancar el sòcol mentre un altre vol utilitzar-lo.

Avaluació

1.- Què s'utilitza per acceptar una nova sol·licitud de connexió d'un programa client?

Scanner

Socket

ServerSocket

InputStream