Socket 入门
1. 什么是Socket
1.1. Socket 的引入
我们知道进程通信的方法有管道、命名管道、信号、消息队列、共享内存、信号量等等,这些方法都要求通信的两个进程位于同一个主机。如果通信双方不在同一个主机,使用 TCP/IP 协议族就能达到我们想要的效果。但是,当我们使用不同的协议进行通信时就得使用不同的接口,还得处理不同协议的各种细节,这就增加了开发的难度,软件也不易于扩展。于是UNIX BSD就发明了socket
1.2. Socket 的位置
socket屏蔽了各个协议的通信细节,使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。这就好比操作系统给我们提供了使用底层硬件功能的系统调用,通过系统调用我们可以方便的使用磁盘(文件操作),使用内存,而无需自己去进行磁盘读写,内存管理。socket 提供了 TCP/IP 协议的抽象,对外提供了一套接口,同过这个接口就可以统一、方便的使用tcp/ip协议的功能了。简单来说, Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket 其实就是一个门面模式,它把复杂的 TCP/IP 协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
<img src=“https://naturalifica.oss-cn-nanjing.aliyuncs.com/~/Users/wuchentian/SoloLearning/Blog/source/imgs202211011749905.png” alt=“图片来源于《tcp/ip协议详解卷一》” style=“zoom: 67%;” />
1.3. Socket 的定义
socket 一词的起源在组网领域的首次使用是在1970年2月12日发布的文献 IETF RFC33 中发现的(第6页),撰写者为 Stephen Carr、Steve Crocker 和 Vint Cerf。根据美国计算机历史博物馆的记载,Croker 写道:“命名空间的元素都可称为套接字接口。一个套接字接口构成一个连接的一端,而一个连接可完全由一对套接字接口规定。(The elements of the name space are called sockets. A socket forms one end of a connection, and a connection is fully specified by a pair of sockets.)”计算机历史博物馆补充道:“这比BSD的套接字接口定义早了大约12年。”
1.4. Socket 的分类
流套接字 (Stream Socket): 主要用于 TCP 协议; 提供了双向的、有序的、无重复的、无记录边界的数据传输服务。
数据报套接字: 主要用于 UDP 协议; 它提供了双向的、无序的、有重复的、有记录边界的数据传输服务。
原始套接字: 主要用于访问底层协议,如 IP、ICMP与IGMP 等协议; 原始套接字可以保存 IP 包中的完整 IP 头部。
sequenceDiagram
Title: 面向连接的客户/服务器的时序图
Note left of Server: socket()
Note left of Server: bind()
Note left of Server: listen()
Note left of Server: accept()
Note right of Client: socket()
Note right of Client: connect()
Note left of Server: 阻塞:等待用户请求
Client ->> Server: 请求建立连接
Client ->> Server: 数据发送
Note left of Server: recv()
Note left of Server: sendto()
Server ->> Client: 数据发送
Note right of Client: recv()
Note left of Server: close()
Note right of Client: close()
sequenceDiagram
Title: 面向无连接的客户/服务器的时序图
Note left of Server: socket()
Note right of Client: socket()
Note left of Server: bind()
Note right of Client: bind()
Note right of Client: sendto()
Client ->> Server: 请求建立连接
Note left of Server: recvfrom()
Note left of Server: 阻塞:等待用户请求
Note left of Server: sendto()
Server ->> Client: 数据发送
Note right of Client: recvfrom()
Note left of Server: close()
Note right of Client: close()
2. Socket 常用函数
注意这里的函数都是包含在 C 语言 <socket.h> 和 <type.h> 头文件中的函数
2.1. socket()
int socket(int protofamily, int so_type, int protocol);
- 点击这里或者查看这篇文章可以查看详情
- protofamily 指协议族,常见的值有:
- AF_INET,指定 so_pcb 中的地址要采用 ipv4 地址类型
- AF_INET6,指定 so_pcb 中的地址要采用 ipv6 的地址类型
- AF_LOCAL/AF_UNIX,指定 so_pcb 中的地址要使用绝对路径名
- so_type 指定 socket 的类型,常见的值有:
- SOCK_STREAM(基于 TCP的,数据传输比较有保障)
- SOCK_DGRAM (是基于 UDP 的,专门用于局域网)
- protocol 指定具体的协议,常见的值有:
- IPPROTO_TCP,TCP 协议
- IPPROTO_UDP,UPD 协议
- 0,如果指定为0,表示由内核根据so_type指定默认的通信协议
值得注意的是,socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而 socket() 用于创建一个 socket 描述符(socket descriptor),它唯一标识一个 socket。这个 socket 描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用 bind() 函数,否则就当调用 connect() 、listen() 时系统会自动随机分配一个端口。
2.2. bind()
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
- addr:一个
const struct sockaddr *
指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同。
- addrlen:对应的是地址的长度。
2.3. listen()
& connect()
int listen(int sockfd, int backlog);
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
如果作为一个服务器,在调用socket()、bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。
listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。
2.4. accept()
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept()
函数的第一个参数为服务器的socket描述字,第二个参数为指向 struct sockaddr *
的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。
如果 accpet
成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的 TCP 连接。
TCP 服务器端依次调用 socket()
bind()
listen()
之后,就会监听指定的 socket 地址了。TCP 客户端依次调用 socket()
connect()
之后就想 TCP 服务器发送了一个连接请求。TCP 服务器监听到这个请求之后,就会调用 accept()
函数取接收请求,这样连接就建立好了。
之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作.
2.5. read()
& write()
网络I/O操作有下面几组:
read();write();
recv();send();
readv();writev();
recvmsg();sendmsg();
recvfrom();sendto();
负责相应的数据读写操作
2.6. close()
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的 socket 描述字,好比操作完打开的文件要调用 fclose()
关闭打开的文件。
3. Java Socket 常用 API
3.1. INetAddress 类 —— 套接字地址/域名解析
import java.net.*;
class Lookup {
public static void main(String[] args) {
try {
InetAddress a = InetAddress.getByName(args[0]);
System.out.println(args[0] + ":" + a.getHostAddress());
} catch (UnknownHostException e) {
System.out.println("No address found for " + args[0]);
}
}
}
----------------------------------------
> java Lookup software.nju.edu.cn
software.nju.edu.cn:219.219.120.45
> java Lookup 127.0.0.1
127.0.0.1:127.0.0.1
-----------------------------------------
3.2. Socket/ServerSocket 类 —— TCP套接字
3.2.1. Socket
Socket 类表示一个建立好的TCP连接,它可以由服务端通过主动连接创建,也可以被 ServerSocket 通过 accept 调用创建(称为主动套接字)
new Socket(InetAddress addr, int port)
InputStream getInputStream()
OutputStream getOutputStream()
void close()
3.2.2. ServerSocket
ServerSocket 类表示服务端创建的等待客户端来连接的TCP套接字(称为被动套接字)
new ServerSocket(int port)
new ServerSocket(int port, int backlog)
new ServerSocket(int port, int backlog, InetAddress bindAddr)
Socket accept()
- InputStream类是 Java 提供的输入流抽象
- 定义于
java.io
包下
- 输入流有流终止和传输错误两种情况,而一般而言前者不抛出异常,后者抛出
java.io.IOException
- 通过
read()
方法进行读取
abstract int read()
int read(byte[] b)
int read(byte[] b, int off, int len)
通过实现 InputStream 类,可以实现新的输入源,如 FileInputStream,ByteBufferInputStream 等
通过包装 InputStream 类,可以扩充和简化 InputStream的API,如 DataInputStream,BufferedInputStream 等
关于更多的Java IO,可以查看这篇文章
3.3.2 OutputStream
OutputStream类为Java提供的输出流抽象
- 定义于
java.io
包下
- 由于数据流的流终止由调用者决定,因此只存在传输错误一种情况,会抛出
java.io.IOException
异常
- 通过
write()
方法进行写出
abstract void write(int b)
void write(byte[] b)
void write(byte[] b, int off, int len)
通过实现 OutputStream 类,可以实现新的输出源,如FileOutputStream,ByteBufferOutputStream 等
通过包装 OutputStream 类,可以扩充和简化 OutputStream 的API,如 DataOutputStream,BufferedOutputStream 等
3.4. DatagramSocket/DatagramPacket —— UDP套接字
3.4.1. DatagramSocket
DatagramSocket 类表示一个 UDP 套接字
new DatagramSocket()
new DatagramSocket(int port)
new DatagramSocket(int port, InetAddress addr)
close()
send(DatagramPacket p)
receive(DatagramPacket p)
setSoTimeout(int timeout)
3.4.2 DatagramPacket
DatagramPacket类封装了一个由UDP套接字传输的数据包,其中包含:
- 一个缓冲区:用于存放UDP报文数据
- 目标机器的IP地址和端口:当DatagramSocket 的 connect 方法被调用后,DatagramPacket 中的目标 IP 和端口将被忽略,直到DatagramSocket 的 disconnect 方法被调用
new DatagramPacket(byte[] buf, int len)
new DatagramPacket(byte[] buf, int len)
4. 包含 TCP/UDP 的 Socket 使用
4.1. TCP 套接字编程实例
- 客户端从标准输入流读取一行字符串,并将其写出到服务端
- 服务端读取客户端的输入数据,将其转换为大写,并传回给客户端
- 客户端从服务端读取数据,并将转换后的字符串输出到标准输出流,回显给用户
package Client
import java.io.*;
import java.net.*;
class TCPClient {
public static void main(String argv[]) throws Exception{
String sentence;
String modifiedSentence;
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
Socket clientSocket = new Socket("hostname", 6789);
DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream());
BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
sentence = inFromUser.readLine();
outToServer.writeBytes(sentence + '\n');
modifiedSentence = inFromServer.readLine();
System.out.println("FROM SERVER: " + modifiedSentence);
clientSocket.close();
}
}
package Server
import java.io.*;
import java.net.*;
class TCPServer {
public static void main(String argv[]) throws Exception{
String clientSentence;
String capitalizedSentence;
ServerSocket welcomeSocket = new ServerSocket(6789);
while(true) {
Socket connectionSocket = welcomeSocket.accept();
BufferedReader inFromClient = new BufferedReader(new InputStreamReader(connectionSocket.getInputStream()));
DataOutputStream outToClient = new DataOutputStream(connectionSocket.getOutputStream());
clientSentence = inFromClient.readLine();
capitalizedSentence = clientSentence.toUpperCase() + '\n';
outToClient.writeBytes(capitalizedSentence);
}
}
}
4.2. UDP 套接字编程实例
package Client
import java.io.*;
import java.net.*;
class UDPClient {
public static void main(String args[]) throws Exception{
BufferedReader inFromUser = new BufferedReader(new InputStreamReader(System.in));
DatagramSocket clientSocket = new DatagramSocket();
InetAddress IPAddress = InetAddress.getByName("hostname");
byte[] sendData = new byte[1024];
byte[] receiveData = new byte[1024];
String sentence = inFromUser.readLine();
sendData = sentence.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, 9876);
clientSocket.send(sendPacket);
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
clientSocket.receive(receivePacket);
String modifiedSentence = new String(receivePacket.getData());
System.out.println("FROM SERVER:" + modifiedSentence);
clientSocket.close();
}
}
package Server
import java.io.*;
import java.net.*;
class UDPServer {
public static void main(String args[]) throws Exception{
DatagramSocket serverSocket = new DatagramSocket(9876);
byte[] receiveData = new byte[1024];
byte[] sendData = new byte[1024];
while(true){
DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
serverSocket.receive(receivePacket);
String sentence = new String(receivePacket.getData());
InetAddress IPAddress = receivePacket.getAddress();
int port = receivePacket.getPort();
String capitalizedSentence = sentence.toUpperCase();
sendData = capitalizedSentence.getBytes();
DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
serverSocket.send(sendPacket);
}
}
}
5. 参考文章
- 深入理解Socket
- AF_INET与套接字
- Java IO最详解