Appearance
TCP Programming
When developing network applications, we encounter the concept of a Socket. A Socket is an abstract concept through which an application establishes a remote connection. Internally, the Socket uses the TCP/IP protocol to transmit data over the network:
┌───────────┐ ┌───────────┐
│Application│ │Application│
├───────────┤ ├───────────┤
│ Socket │ │ Socket │
├───────────┤ ├───────────┤
│ TCP │ │ TCP │
├───────────┤ ┌──────┐ ┌──────┐ ├───────────┤
│ IP │◀───▶│Router│◀────▶│Router│◀───▶│ IP │
└───────────┘ └──────┘ └──────┘ └───────────┘
The functionalities of Socket, TCP, and some aspects of IP are provided by the operating system. Different programming languages offer simple wrappers around these operating system calls. For example, Java provides several Socket-related classes that encapsulate the interfaces provided by the operating system.
Why Use Sockets for Network Communication?
Communicating solely through IP addresses is insufficient because a single computer can run multiple network applications simultaneously, such as browsers, QQ, email clients, etc. When the operating system receives a data packet, it cannot determine which application to forward it to based only on the IP address. Therefore, the operating system abstracts the Socket interface. Each application corresponds to a different Socket, allowing data packets to be correctly routed to the appropriate application based on the Socket.
A Socket consists of an IP address and a port number (ranging from 0 to 65535). You can think of a Socket as an IP address combined with a port number. Port numbers are always assigned by the operating system and are numerical values between 0 and 65535. Ports below 1024 are privileged and require administrator permissions, while ports above 1024 can be opened by any user-level application.
Examples:
- Browser: 101.202.99.2:1201
- QQ: 101.202.99.2:1304
- Email: 101.202.99.2:15000
When performing network programming with Sockets, it essentially involves network communication between two processes. One process must act as the server, actively listening on a specified port, while the other process must act as the client, actively connecting to the server's IP address and specified port. If the connection is successful, the server and client establish a TCP connection, allowing both parties to send and receive data at any time.
Therefore, once a Socket connection is successfully established between the server and client:
- Server Side: The Socket consists of the specified IP address and specified port number.
- Client Side: The Socket consists of the client's computer IP address and a randomly assigned port number by the operating system.
Server Side
To perform Socket programming, we first need to write the server-side program. The Java standard library provides ServerSocket
to implement listening on a specified IP and port. A typical implementation of ServerSocket
is as follows:
java
public class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(6666); // Listen on specified port
System.out.println("server is running...");
for (;;) {
Socket sock = ss.accept();
System.out.println("connected from " + sock.getRemoteSocketAddress());
Thread t = new Handler(sock);
t.start();
}
}
}
class Handler extends Thread {
Socket sock;
public Handler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("client disconnected.");
}
}
private void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
writer.write("hello\n");
writer.flush();
for (;;) {
String s = reader.readLine();
if (s.equals("bye")) {
writer.write("bye\n");
writer.flush();
break;
}
writer.write("ok: " + s + "\n");
writer.flush();
}
}
}
The server side uses the code:
java
ServerSocket ss = new ServerSocket(6666);
to listen on the specified port 6666. Here, we did not specify an IP address, which means it listens on all network interfaces of the computer.
If the ServerSocket
listens successfully, we use an infinite loop to handle client connections:
java
for (;;) {
Socket sock = ss.accept();
Thread t = new Handler(sock);
t.start();
}
Notice that the method ss.accept()
returns a Socket
instance each time a new client connects. This Socket
instance is used to communicate with the newly connected client. Since there may be many clients, to achieve concurrent processing, we must create a new thread for each new Socket
to handle it. This way, the main thread's role is to accept new connections and spawn a new thread for each connection.
We have introduced thread pools in the multithreading programming section. Here, we can also utilize thread pools to handle client connections, significantly improving operational efficiency.
If no client connects, the accept()
method will block and wait indefinitely. If multiple clients connect simultaneously, ServerSocket
will queue the connections and handle them one by one. For Java programs, simply calling accept()
in a loop allows you to obtain new connections continuously.
Client Side
Compared to the server side, the client program is much simpler. A typical client program is as follows:
java
public class Client {
public static void main(String[] args) throws IOException {
Socket sock = new Socket("localhost", 6666); // Connect to specified server and port
try (InputStream input = sock.getInputStream()) {
try (OutputStream output = sock.getOutputStream()) {
handle(input, output);
}
}
sock.close();
System.out.println("disconnected.");
}
private static void handle(InputStream input, OutputStream output) throws IOException {
var writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
var reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
Scanner scanner = new Scanner(System.in);
System.out.println("[server] " + reader.readLine());
for (;;) {
System.out.print(">>> "); // Print prompt
String s = scanner.nextLine(); // Read a line of input
writer.write(s);
writer.newLine();
writer.flush();
String resp = reader.readLine();
System.out.println("<<< " + resp);
if (resp.equals("bye")) {
break;
}
}
}
}
The client program connects to the server using:
java
Socket sock = new Socket("localhost", 6666);
Note that the server address in the above code is "localhost"
, which refers to the local machine address, and the port number is 6666
. If the connection is successful, a Socket
instance is returned for subsequent communication.
Socket Streams
Once a Socket connection is successfully established, both the server and the client use the Socket instance for network communication. Since TCP is a stream-based protocol, the Java standard library uses InputStream
and OutputStream
to encapsulate the Socket's data streams, allowing us to use Socket streams similarly to regular IO streams:
java
// For reading network data:
InputStream in = sock.getInputStream();
// For writing network data:
OutputStream out = sock.getOutputStream();
Finally, let's focus on why we need to call the flush()
method when writing network data.
If flush()
is not called, we might find that neither the client nor the server receives the data. This is not a design issue with the Java standard library. Instead, when we write data in the form of a stream, the data is not immediately sent over the network upon writing. Instead, it is first written to an internal buffer. The data is only sent over the network once the buffer is full. This design aims to improve transmission efficiency. If the buffered data is minimal and we want to force these data to be sent over the network, we must call flush()
to forcibly send the buffered data.
Exercise
Implement server and client communication using Sockets.
Summary
When performing TCP programming in Java, the Socket model is required:
- Server Side: Use
ServerSocket
to listen on a specified port. - Client Side: Use
Socket(InetAddress, port)
to connect to the server. - Server Side: Use
accept()
to receive connections and return aSocket
. - Both Sides: Use
InputStream
/OutputStream
from the Socket to read and write data. - Server Side: Typically uses multithreading to handle multiple client connections simultaneously. Utilizing a thread pool can greatly enhance efficiency.
flush()
: Used to forcibly send the buffer's contents to the network.