您的位置:首页 > 运维架构 > Linux

简单hello/hi程序、分析及Java Socket API与Linux Socket API对比

2019-12-10 13:08 411 查看

1.Socket 定义

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

传输层实现端到端的通信,因此,每一个传输层连接由两个端点/。那么,传输层连接的断电是什么呢,不是主机,不是主机的IP地址,不是应用进程,也不是传输层的协议端口,传输层连接的端点叫做套接字(socket)。根据RFC793的定义,端口号拼接到IP地址就构成了套接字。所谓套接字,实际上是一个通信端点,每个套接字都有一个套接字序号,包括主机的IP地址与一个16为的主机端口号,即形如(主机IP地址:端口号)。例如,如果IP地址是210.37.145.1,而端口号是23,那么得到套接字就是(210.37.145.1:23).总之,套接字Socket=(IP地址:端口号),套接字的表示方法是点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开。每一个传输层链接唯一地被通信两端的两个端点(即两个套接字)所确定。

2. Hello/Hi

下面用Java简单的实现一个基于Socket通信的hello/hi程序:

Server端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
class Server {
private Socket server;
private Server() {
try {
System.out.println("启动服务器!");
ServerSocket serverSocket = new ServerSocket(8888);
server = serverSocket.accept();
} catch (IOException e) {
e.printStackTrace();
}
}
private void listen() {
try {
System.out.println("Listening!......");
//从Socket中获得输入流
InputStreamReader in = new InputStreamReader(server.getInputStream());
BufferedReader br = new BufferedReader(in);
//读取输入流中的一行并输出
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
private void send(String msg) {
try {
PrintWriter out = new PrintWriter(server.getOutputStream(), true);
out.println("Server:" + msg);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server se = new Server();
String msg = "";
Scanner cin = new Scanner(System.in);
while (!msg.equals("#")) {
se.listen();
System.out.print("输入信息:");
msg = cin.nextLine();
se.send(msg);
}
}
}

Client端:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
class Client {
private Socket client;
private Client() {
try {
client = new Socket("127.0.0.1", 8888);
} catch (Exception e) {
e.printStackTrace();
}
}
private void send(String msg) {
try {
PrintWriter out = new PrintWriter(client.getOutputStream(), true);
out.println("Client:" + msg);
} catch (IOException e) {
e.printStackTrace();
}
}
private void listen() {
try {
System.out.println("Listening!......");
InputStreamReader in = new InputStreamReader(client.getInputStream());
BufferedReader br = new BufferedReader(in);
System.out.println(br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String msg = "";
Client c = new Client();
Scanner cin = new Scanner(System.in);
while (!msg.equals("#")) {
System.out.print("Input: ");
msg = cin.nextLine();
c.send(msg);
c.listen();
}
}
}

执行结果:

Client发送hello,Server回应hi 

            

 调用栈分析:

为什么java实现socket通信这么方便呢,这就需要我们深入源码去一探究竟了,这里以Server端为例,追踪调用栈:

实例化ServerSocket时,构造函数会调用ServerSocket的bind()方法,

public ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException {
...if (port >= 0 && port <= 65535) {
if (backlog < 1) {
backlog = 50;
}
try {
this.bind(new InetSocketAddress(bindAddr, port), backlog);
} catch (SecurityException var5) {
this.close();
throw var5;
} catch (IOException var6) {
this.close();
throw var6;
}
} else {
throw new IllegalArgumentException("Port value out of range: " + port);
}
}
public void bind(SocketAddress endpoint, int backlog) throws IOException {
...try {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkListen(epoint.getPort());
}

this.getImpl().bind(epoint.getAddress(), epoint.getPort());
this.getImpl().listen(backlog);
this.bound = true;
} catch (SecurityException var5) {
this.bound = false;
throw var5;
} catch (IOException var6) {
this.bound = false;
throw var6;
}
...
}

该方法会调用继承自抽象类AbstractPlainSocketImpl的PlainSocketImpl的socketBind()方法,在该方法中会调用native方法bind0(),从而实现将一个socket连接绑定到指定的本地IP地址和端口号。

注:native关键字标注的方法为本地方法,一般是用其他语言写成的函数,常用来实现java语言对OS底层接口的访问。Java语言本身不能直接对操作系统底层进行操作,但是java允许程序通过Java本机接口JNI,使用C/C++等其他语言实现这种操作。在Windows系统中,使用native关键字标注的本地方法在编译时会生成一个动态链接库(.dll文件)为Java语言提供响应的本地服务。

void socketBind(InetAddress address, int port) throws IOException {
int nativefd = this.checkAndReturnNativeFD();
if (address == null) {
throw new NullPointerException("inet address argument is null.");
} else if (preferIPv4Stack && !(address instanceof Inet4Address)) {
throw new SocketException("Protocol family not supported");
} else {
bind0(nativefd, address, port, useExclusiveBind);
if (port == 0) {
this.localport = localPort0(nativefd);
} else {
this.localport = port;
}

this.address = address;
}
}

接着,同样的步骤从ServerSocket的listen()方法可以一直追溯到PlainSocketImpl的sokectListen()方法的listen0(),该方法主要为了设置允许的最大连接请求队列长度,当请求队列满时,拒绝后来的连接请求。

最后,同样,从ServerSocket类的accept()追溯到accept0(),等待连接请求的到来。

具体调用关系如下图所示:(图转自https://www.geek-share.com/detail/2786726480.html

 

3. Java Socekt API与Linux Socket API对比

Linux提供的响应Socket API在sys/socket.h中,分别为:

int socket(int domain, int type, int protocol);

从函数名就可以看出,socket函数可以创建一个socket,

其中,domain参数告诉系统使用哪个底层协议族,对TCP/IP协议族而言,该参数应该设置为PF_INET或PF_INET6,没错,分别对应IPv4和IPv6,对于UNIX本地域协议族而言,该参数应该设置为PF_UNIX,具体socket系统支持的所有协议族,请读者自行参考其man手册。

type参数指定服务类型,主要有SOCK_STREAM流服务和SOCK_UGRAM数据报服务,对TCP/IP协议族而言,其值取SOCK_STREAM表示传输层使用TCP协议,取SOCK_DGRAM表示传输层使用UDP协议。

protocol参数是在前两个参数构成的协议集合下,再选择一个具体的协议。不过这个值通常是唯一的,几乎在所有情况下,我们都应该把它设置为0,表示使用默认协议。

熟悉UNIX/Linux的同学应该知道,在这类系统中,所有的东西都是文件,socket也不例外,可读,可写,可控制,可关闭的文件描述符。socket函数调用成功时返回一个socket文件描述符

int bind(int sockfd, const struct sockaddr *addr, socklen_t addelen)

bind将my_addr所指的socket地址分配给未命名的socketfd文件描述符,addrlen参数指出该socket地址的长度。bind成功时返回0,失败则返回-1并设置errno,常见为EACCES和EASSRINUSE,前者代表被绑定的地址是受保护的地址,仅超级用户能够访问,后者表示被绑定的地址正在使用中。

值得注意的是,Client端通常不需要bind socket而是采用匿名方式,OS自动分配socket地址。

int listen(int sockfd, int backlog);

socket被bind之后还不能马上接收客户的连接,需要创建一个监听队列存放待处理的客户连接,服务端通过listen进行监听。

sockfd参数指定被监听的socket,backlog参数体时内核监听队列的最大长度,如果超过,服务器将不再受理新的客户端连接,客户端也将收到ECONNREFUSED错误信息。listen成功返回0,失败返回-1并设置errno。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

最后一步为accept,其中sockfd参数是执行过listen系统调用的监听socket,addr参数用来获取被接受连接的远程socket地址,该socket地址的长度由addlen参数指出,accpet成功时返回一个新的连接socket,该socket唯一地标识了被接受的这个连接,服务器可通过读写该socket来与被接受连接对应的客户端通信,accept失败时返回-1并设置errno。

 

其实,Java也是调用Linux网络API实现网络通信的,通过调用这些系统API来实现它的底层功能的,从调用分析时贴出的源码中可以看出,在Java的ServerSocket创建时就对方法进行了socket的bind和listen操作,一个方法就封装了3个API,即ServerSocket的实例化过程就对应了Linux中的socket(),bind(),listen(),而Java中的accept对应了Linux的accept函数,相关对应关系如下图所示(图来源https://blog.csdn.net/vipshop_fin_dev/article/details/102966081):

 所以,Java将这一切全都封装起来,这使得面向网络的编程对于Java程序员来说变得十分简单,我们只需要知道使用的哪一个类(实际上也就是ServerSocket和Socket两个类),为它们传入必要的地址参数,就能够轻松实现Socket通信。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  bind inet