深入分析Java Socket 原理之阻塞套接字

1,713 阅读4分钟

0、套接字(Socket)介绍

0.1 套接字是什么?

  1. 套接字类似于unix中的管道,我们既可以往里面写数据(套接字发送缓冲区),也可以从中读取数据(套接字接收缓冲区);
  2. 套接字工作在传输层与应用层之间,主要是为应用程序提供网络I/O的能力;
  3. Sockets API通过套接字描述符去定位要访问套接字文件; 比如说要写入的套接字的套接字描述符为5,那程序便可以通过描述符5去访问这个套接字文件。

0.2 简单的一次Socket通信的图:

省略一些维护缓冲区的细节:

即我们的程序通过fgets()将要传输的数据传入标准输入文件中,并写入套接字发送缓冲区,再经过TCP按照MSS(最大报文长度)等选项分包,再经过IP协议族的转发至服务端的相应的套接字接收缓冲区,然后复制到应用程序可以用于输出的标准输出文件中进行相应的处理。

一、一次基于TCP的网络通信的过程:

1.1 服务器启动,并准备监听:

int  listenfd, connfd;
struct sockaddr_in	servaddr;
char buff[MAXLINE];
//创建一个TCP套接字并返回一个套接字描述符
listenfd = Socket(AF_INET, SOCK_STREAM, 0);

//设置IPV4的套接字地址结构
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family      = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port        = htons(13);	/* daytime server */

//按照套接字地址结构设置服务器监听信息
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));

//将套接字设置为被动打开,即处于等待客户端发起三次握手的状态
Listen(listenfd, LISTENQ);

1.2 客户端构建套接字文件并发起连接:

首先构建套接字

//存储套接字描述符,Sockets API通过此描述符对套接字文件进行操作;
int sockfd; 
//使用socket函数创建网际的、面向字节流的套接字(即TCP套接字),并返回套接字描述符
if ( (sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	err_sys("socket error");

然后构建IPV4的套接字地址结构

//IPV4套接字地址结构,主要用于设置套接字的属性
struct sockaddr_in	servaddr;

bzero(&servaddr, sizeof(servaddr));
//设置协议为网际协议,网际协议即为IP协议族
servaddr.sin_family = AF_INET;
//设置要访问的服务器端口号为13
servaddr.sin_port   = htons(13);
//argv[1]是点分十进制IP,如192.168.0.1
//inet_pton函数将其转变为适合在网络上传输的二进制流
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
    err_quit("inet_pton error for %s", argv[1]);

然后通过connect()函数连接服务器

//传入的参数1. sockfd为套接字描述符;
//2.3. IPV4套接字地址结构和结构体的长度
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0)
	err_sys("connect error");

1.3 服务器接受连接并返回响应:

样例是比较简单的服务器,一次只能处理一个连接,再往后一点可以使用fork()函数创建新进程使用线程代替进程,并使用线程池,或者改用非阻塞模式等等。

监听套接字维护着一个连接队列,主要用于提供给Accept()函数去处理已完成三次握手的套接字的请求;

//监听套接字以及已连接套接字描述符
int	listenfd, connfd;
//应用进程缓冲区
char	buff[MAXLINE];
time_t	ticks;
for ( ; ; ) {
//调用Accept()函数处理Connect队列中已完成连接的套接字的请求
//并对已连接套接字描述符进行赋值
    connfd = Accept(listenfd, (SA *) NULL, NULL);

    ticks = time(NULL);
    //向缓冲区中写要返回给客户端的数据
    snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
    //将缓冲区中数据通过write()函数系统调用写入到套接字中
    Write(connfd, buff, strlen(buff));
    //close()函数通过四次挥手关闭与客户端的连接
    Close(connfd);
}

1.4 客户端接收服务端响应并进行相应处理:

char	recvline[MAXLINE + 1];
//调用`read()函数`读取套接字接收缓冲区,并放入recvline缓冲区中
while ( (n = read(sockfd, recvline, MAXLINE)) > 0) {
	recvlin	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port   = htons(13);	/* daytime server */
	if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0)
	    err_quit("inet_pton error for %s", argv[1]);
	    e[n] = 0;	/* null terminate */
	    //将recvline中的数据放入标准输出文件中,并使用I/O函数fputs()输出结果
            if (fputs(recvline, stdout) == EOF)
		err_sys("fputs error");
}