Socket是一个抽象概念,一个应用程序通过一个Socket来建立一个远程连接,而Socket内部通过TCP/IP协议把数据传输到网络:
Socket、TCP和部分IP的功能都是由操作系统提供的,不同的编程语言只是提供了对操作系统调用的简单的封装。例如,Java提供的几个Socket相关的类就封装了操作系统提供的接口。
为什么需要Socket进行网络通信?因为仅仅通过IP地址进行通信是不够的,同一台计算机同一时间会运行多个网络应用程序,例如浏览器、QQ、邮件客户端等。当操作系统接收到一个数据包的时候,如果只有IP地址,它没法判断应该发给哪个应用程序,所以,操作系统抽象出Socket接口,每个应用程序需要各自对应到不同的Socket,数据包才能根据Socket正确地发到对应的应用程序。
一个Socket就是由IP地址和端口号(范围是0~65535)组成,可以把Socket简单理解为IP地址加端口号。端口号总是由操作系统分配,它是一个0~65535之间的数字,其中,小于1024的端口属于特权端口,需要管理员权限,大于1024的端口可以由任意用户的应用程序打开。
使用Socket进行网络编程时,本质上就是两个进程之间的网络通信。其中一个进程必须充当服务器端,它会主动监听某个指定的端口,另一个进程必须充当客户端,它必须主动连接服务器的IP地址和指定端口,如果连接成功,服务器端和客户端就成功地建立了一个TCP连接,双方后续就可以随时发送和接收数据。
因此,当Socket连接成功地在服务器端和客户端之间建立后:
要使用Socket编程,我们首先要编写服务器端程序。Java标准库提供了ServerSocket
来实现对指定IP和指定端口的监听。
服务器端
package com.learn.socket;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
class Handle extends Thread{
Socket socket;
public Handle(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try(InputStream inputStream=this.socket.getInputStream()){
try(OutputStream outputStream=this.socket.getOutputStream()){
System.out.println(inputStream);
System.out.println(outputStream);
handle(inputStream,outputStream);
}
}catch (Exception e){
try{
this.socket.close();
}catch (IOException ioe){
}
System.out.println("client disconnected.");
}
}
private void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
System.out.println(inputStream);
System.out.println(outputStream);
BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream,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();
}
}
}
public class Server {
public static void main(String[] args) throws IOException {
//指定监听端口
ServerSocket serverSocket=new ServerSocket(6666);
System.out.println("server is running");
for(;;){
Socket socket=serverSocket.accept();
System.out.println("connected from "+socket.getRemoteSocketAddress());
Thread thread=new Handle(socket);
thread.start();
}
}
}
服务器端通过代码:
//指定监听端口
ServerSocket serverSocket=new ServerSocket(6666);
在指定端口6666监听。这里我们没有指定IP地址,表示在计算机的所有网络接口上进行监听。
如果ServerSocket
监听成功,我们就使用一个无限循环来处理客户端的连接:
for(;;){
Socket socket=serverSocket.accept();
System.out.println("connected from "+socket.getRemoteSocketAddress());
Thread thread=new Handle(socket);
thread.start();
}
注意到代码serverSocket.accept()
表示每当有新的客户端连接进来后,就返回一个Socket
实例,这个Socket
实例就是用来和刚连接的客户端进行通信的。由于客户端很多,要实现并发处理,我们就必须为每个新的Socket
创建一个新线程来处理,这样,主线程的作用就是接收新的连接,每当收到新连接后,就创建一个新线程进行处理。
这里也完全可以利用线程池来处理客户端连接,能大大提高运行效率。
如果没有客户端连接进来,accept()
方法会阻塞并一直等待。如果有多个客户端同时连接进来,ServerSocket
会把连接扔到队列里,然后一个一个处理。对于Java程序而言,只需要通过循环不断调用accept()
就可以获取新的连接。
客户端
package com.learn.socket;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class Client {
private static void handle(InputStream inputStream, OutputStream outputStream) throws IOException {
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
BufferedWriter writer=new BufferedWriter(new OutputStreamWriter(outputStream,StandardCharsets.UTF_8));
Scanner scanner=new Scanner(System.in);
System.out.println("[server] "+reader.readLine());
for(;;){
System.out.println(">>>");
String s=scanner.nextLine();
writer.write(s);
writer.newLine();
writer.flush();
String resp=reader.readLine();
System.out.println("<<<"+resp);
if(resp.equals("bye")){
break;
}
}
}
public static void main(String[] args) throws IOException {
Socket socket=new Socket("localhost",6666);
try(InputStream inputStream=socket.getInputStream()){
try(OutputStream outputStream=socket.getOutputStream()){
handle(inputStream,outputStream);
}
}
socket.close();
System.out.println("disconnected.");
}
}
客户端程序通过:
Socket socket=new Socket("localhost",6666);
连接到服务器端,注意上述代码的服务器地址是"localhost"
,表示本机地址,端口号是6666
。如果连接成功,将返回一个Socket
实例,用于后续通信。
Socket流
当Socket连接创建成功后,无论是服务器端,还是客户端,我们都使用Socket
实例进行网络通信。因为TCP是一种基于流的协议,因此,Java标准库使用InputStream
和OutputStream
来封装Socket的数据流,这样我们使用Socket的流,和普通IO流类似:
// 用于读取网络数据:
InputStream in = sock.getInputStream();
// 用于写入网络数据:
OutputStream out = sock.getOutputStream();
为什么写入网络数据时,要调用flush()
方法。
如果不调用flush()
,我们很可能会发现,客户端和服务器都收不到数据,这并不是Java标准库的设计问题,而是我们以流的形式写入数据的时候,并不是一写入就立刻发送到网络,而是先写入内存缓冲区,直到缓冲区满了以后,才会一次性真正发送到网络,这样设计的目的是为了提高传输效率。如果缓冲区的数据很少,而我们又想强制把这些数据发送到网络,就必须调用flush()
强制把缓冲区数据发送出去。
测试结果如下:
注:
使用Java进行TCP编程时,需要使用Socket模型:
ServerSocket
监听指定端口;Socket(InetAddress, port)
连接服务器;accept()
接收连接并返回Socket
;Socket
打开InputStream
/OutputStream
读写数据;flush()
用于强制输出缓冲区到网络。
计算机网络是指两台或更多的计算机组成的网络,在同一个网络中,任意两台计算机都可以直接通信,因为所有计算机都需要遵循同一种网络协议。
那什么是互联网呢?互联网是网络的网络(internet),即把很多计算机网络连接起来,形成一个全球统一的互联网。
对某个特定的计算机网络来说,它可能使用网络协议ABC,而另一个计算机网络可能使用网络协议XYZ。如果计算机网络各自的通讯协议不统一,就没法把不同的网络连接起来形成互联网。因此,为了把计算机网络接入互联网,就必须使用TCP/IP协议。
TCP/IP协议泛指互联网协议,其中最重要的两个协议是TCP协议和IP协议。只有使用TCP/IP协议的计算机才能够联入互联网,使用其他网络协议(例如NetBIOS、AppleTalk协议等)是无法联入互联网的。
IP协议是一个分组交换,它不保证可靠传输。而TCP协议是传输控制协议,它是面向连接的协议,支持可靠传输和双向通信。TCP协议是建立在IP协议之上的,简单地说,IP协议只负责发数据包,不保证顺序和正确性,而TCP协议负责控制数据包传输,它在传输数据之前需要先建立连接,建立连接后才能传输数据,传输完后还需要断开连接。TCP协议之所以能保证数据的可靠传输,是通过接收确认、超时重传这些机制实现的。并且,TCP协议允许双向通信,即通信双方可以同时发送和接收数据。
TCP协议也是应用最广泛的协议,许多高级协议都是建立在TCP协议之上的,例如HTTP、SMTP等。
UDP协议(User Datagram Protocol)是一种数据报文协议,它是无连接协议,不保证可靠传输。因为UDP协议在通信前不需要建立连接,因此它的传输效率比TCP高,而且UDP协议比TCP协议要简单得多。
选择UDP协议时,传输的数据通常是能容忍丢失的,例如,一些语音视频通信的应用会选择UDP协议。