Socket 入门

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 CarrSteve CrockerVint 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 协议; 它提供了双向的无序的有重复的有记录边界的数据传输服务

原始套接字: 主要用于访问底层协议IPICMP与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_TCPTCP 协议
    • IPPROTO_UDPUPD 协议
    • 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()函数创建了唯一标识一个socketbind()函数就是将给这个描述字绑定一个名字
  • 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()

int close(int fd);

在服务器与客户端建立连接之后会进行一些读写操作完成了读写操作就要关闭相应的 socket 描述字好比操作完打开的文件要调用 fclose() 关闭打开的文件

3. Java Socket 常用 API

3.1. INetAddress 类 —— 套接字地址/域名解析

  • Java使用InetAddress表示IP地址

    • 定义于 java.net 包下

    • 既可以表示 IPv4 的地址也可以表示 IPv6 的地址

  • 在 Java 的 Socket API 中往往将一个 InetAddress 对象和一个端口号一起使用作为套接字地址

  • 通过 InetAddress 的静态方法 getByName() 可以将一个IP地址或域名转换为 InetAddress 对象

    • 当对应域名不存在或无法解析时会抛出 java.net.UnknownHostException 异常需要对其进行处理
  • 举例如下

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)
//当目标机器不可达<span class="bd-box"><h-char class="bd bd-beg"><h-inner>、</h-inner></h-char></span>连接被重置或拒绝等情况时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>会抛出java.io.IOException异常

  
//与远程机器通信
InputStream getInputStream()//从远程机器读数据
  
OutputStream getOutputStream()//向远程机器写数据
  
void close()//关闭该套接字连接

3.2.2. ServerSocket

ServerSocket 类表示服务端创建的等待客户端来连接的TCP套接字称为被动套接字

//绑定ServerSocket到本地端口并监听
new ServerSocket(int port) //backlog=50
  
new ServerSocket(int port, int backlog)
  
new ServerSocket(int port, int backlog, InetAddress bindAddr)
  
//当绑定失败<span class="bd-box"><h-char class="bd bd-end"><h-inner>(</h-inner></h-char></span>譬如端口被占用时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>)</h-inner></h-char></span>均抛出java.io.IOException

//通过accept调用获取一个完成三次握手的TCP连接
Socket accept()
//创建时的backlog参数表示允许完成三次握手但没被accept调用获取到的TCP连接个数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>超出backlog的连接将会被拒绝

3.3. InputStream/OutputStream —— 数据的读取和写出

3.3.1. InputStream

  • InputStream类是 Java 提供的输入流抽象
    • 定义于 java.io 包下
    • 输入流有流终止和传输错误两种情况而一般而言前者不抛出异常后者抛出 java.io.IOException
    • 通过 read() 方法进行读取
abstract int read()//从输入流读取单个字节<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当读取成功时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>返回0-255的整数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当流终止时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>返回-1
  
int read(byte[] b)//从输入流读取最多b.length个字节<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>并返回读取到b的元素的个数<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当流终止时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>返回-1
  
int read(byte[] b, int off, int len)//从输入流读取最多len个字节到b中以b[off]开头的存储空间中<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>当流终止时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>返回-1

通过实现 InputStream 类可以实现新的输入源如 FileInputStreamByteBufferInputStream 等

通过包装 InputStream 类可以扩充和简化 InputStream的API如 DataInputStreamBufferedInputStream 等

关于更多的Java IO可以查看这篇文章

3.3.2 OutputStream

OutputStream类为Java提供的输出流抽象

  • 定义于 java.io 包下
  • 由于数据流的流终止由调用者决定因此只存在传输错误一种情况会抛出 java.io.IOException 异常
  • 通过 write() 方法进行写出
abstract void write(int b)//写出单个字节到输出流<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>只有低8位有效
  
void write(byte[] b)//将b中b.length个字节写出到输出流
  
void write(byte[] b, int off, int len)//将b中b[off]开头的len个字节写出到输出流

//通过close方法关闭/终止输出流
//子类通过重写close方法决定close时的行为

通过实现 OutputStream 类可以实现新的输出源如FileOutputStreamByteBufferOutputStream 等

通过包装 OutputStream 类可以扩充和简化 OutputStream 的API如 DataOutputStreamBufferedOutputStream 等

3.4. DatagramSocket/DatagramPacket —— UDP套接字

3.4.1. DatagramSocket

DatagramSocket 类表示一个 UDP 套接字

//绑定UDP套接字到本地端口
//当绑定失败时<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>均会抛出java.net.SocketException异常
new DatagramSocket()//绑定套接字到任意端口
new DatagramSocket(int port)//绑定套接字到指定端口
new DatagramSocket(int port, InetAddress addr)//绑定套接字到指定IP地址和端口


close()//关闭当前UDP套接字<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>释放其占用的端口

  
//发送和接收数据报文
send(DatagramPacket p)//发送UDP报文
receive(DatagramPacket p)//接收UDP报文<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>会一直阻塞接收到报文<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>或者在设置了超时时间后<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>抛出java.net.SocketTimeoutException
setSoTimeout(int timeout)//设置接收UDP报文的超时时间

3.4.2 DatagramPacket

DatagramPacket类封装了一个由UDP套接字传输的数据包其中包含

  • 一个缓冲区用于存放UDP报文数据
  • 目标机器的IP地址端口当DatagramSocket 的 connect 方法被调用后DatagramPacket 中的目标 IP 和端口将被忽略直到DatagramSocket 的 disconnect 方法被调用
//创建DatagramPacket的载荷

new DatagramPacket(byte[] buf, int len)//创建一个用于接收的DatagramPacket类<span class="bd-box"><h-char class="bd bd-beg"><h-inner>,</h-inner></h-char></span>超出len的UDP数据将被截断
new DatagramPacket(byte[] buf, int len)//创建一个用于发送的DatagramPacket类

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. 参考文章

  1. 深入理解Socket
  2. AF_INET与套接字
  3. Java IO最详解