CSAPP学习笔记——Chapter10,11 系统级I/O与网络编程

Chapter10 系统级I/O

系统级I/O这一章的内容,主要可以通过这张图概括:

Unix I/O模型是在操作系统内核中实现的。应用程序可以通过诸如 open、close、lseek、read、write 和 stat 这样的函数来访 Unix I/O。较高级别的 RIO 和标准I/O函数都是基于(使用)Unix I/O 函数来实现的。RIO函数是专为本书开发的 read 和 write的健壮的包装函数。它们自动处理不足值,并且为读文本行提供一种高效的带缓冲的方法。标准I/O 函数提供了 Unix I/O 函数的一个更加完整的带缓冲的替代品,包括格式化的I/O例程,如printf和 scanf。

这里面有一个知识点,缓冲相较于没有缓冲的Unix I/O好在什么地方?

下面我会围绕这这个问题介绍一下Unix I/O、RIO、标准I/O函数。

Unix I/O

read

read 函数从描述符为 fd 的当前文件位置复制最多n个字节到内存位置 buf。返回值-1表示一个错误,而返回值 0 表示 EOF。否则,返回值表示的是实际传送的字节数量。

返回值

  • 成功时,返回实际读取的字节数,这个值可能小于请求的count,特别是在读取普通文件时到达文件末尾,或者在读取网络数据时网络缓慢等情况下。
  • 如果读取到文件末尾(EOF),返回0
  • 出错时,返回-1,并设置errno以指示错误类型。

阻塞行为

read调用的阻塞行为取决于文件描述符的状态和读取操作的上下文:

  • 对于普通文件,如果请求的count字节可用,read通常会读取所请求的字节数并返回。如果到达文件末尾,返回的字节数可能会少于请求的count,或者在完全到达末尾时返回0
  • 对于网络套接字和管道,如果没有数据可读,read会阻塞,直到有数据到达、连接关闭、或者收到信号中断read调用。
  • 文件描述符可以被设置为非阻塞模式。在这种模式下,如果read操作会阻塞,它会立即返回-1,并且errno被设置为EAGAINEWOULDBLOCK

错误处理

read函数在出错时返回-1,具体的错误原因可以通过检查errno值来确定。一些常见的错误包括:

  • EINTR:读取操作被信号中断。
  • EAGAINEWOULDBLOCK:在非阻塞模式下,当前没有数据可读。
  • EBADFfd不是一个有效的文件描述符或不是打开的读取。
  • EFAULTbuf指向的缓冲区不可访问。

write

write 函数从内存位置 buf 复制至多n个字节到描述符 的当前文件位置。

这两个Unix I/O函数是没有缓冲区的,也就是每次读写都是内存和文件的直接交互。

RIO函数

RIO(Robust I/O)提供了两种不同的函数:

  • 无缓冲的输入输出函数。这些函数直接在内存和文件之间传送数据,没有应用级缓冲。它们对将二进制数据读写到网络和从网络读写二进制数据尤其有用。
  • 带缓冲的输入函数。这些函数允许你高效地从文件中读取文本行和二进制数据,这些文件的内容缓存在应用级缓冲区内,类似于为 printf 这样的标准 I/0函数提供的缓冲区。与[110]中讲述的带缓冲的I/0 例程不同,带缓冲的 RIO 输入函数是线程安全的,它在同一个描述符上可以被交错地调用。例如,你可以从一个描述符中读一些文本行,然后读取一些二进制数据,接着再多读取一些文本行。

因为我们的读写主要是文本文件,所以第一类函数不在今天的讨论范围。

缓冲区定义
/* Persistent state for the robust I/O (Rio) package */
/* $begin rio_t */
#define RIO_BUFSIZE 8192
typedef struct {
int rio_fd; /* 文件描述符,也就是被缓冲的文件*/
int rio_cnt; /* Unread bytes in internal buf */
char *rio_bufptr; /* Next unread byte in internal buf */
char rio_buf[RIO_BUFSIZE]; /* Internal buffer */
} rio_t;
/* $end rio_t */
初始化缓冲区函数
/* $begin rio_readinitb */
void rio_readinitb(rio_t *rp, int fd)
{
rp->rio_fd = fd;
rp->rio_cnt = 0;
rp->rio_bufptr = rp->rio_buf;
}
/* $end rio_readinitb */

当读写缓冲区初始化之后,

rio_read

还需要看一个rio_read()的源码,这个是基于Unix I/O read编写的带缓冲版本:

/* $begin rio_read */
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
// rio_t 缓冲区
// userbuf 用户存放数据的存放地
// 用户希望读取的字节数
int cnt; while (rp->rio_cnt <= 0) { /* Refill if buf is empty */
rp->rio_cnt = read(rp->rio_fd, rp->rio_buf,
sizeof(rp->rio_buf));
if (rp->rio_cnt < 0) {
if (errno != EINTR) /* Interrupted by sig handler return */
return -1;
}
else if (rp->rio_cnt == 0) /* EOF */
return 0;
else
rp->rio_bufptr = rp->rio_buf; /* Reset buffer ptr */
} /* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */
cnt = n;
if (rp->rio_cnt < n)
cnt = rp->rio_cnt;
memcpy(usrbuf, rp->rio_bufptr, cnt);
rp->rio_bufptr += cnt;
rp->rio_cnt -= cnt;
return cnt;
}
/* $end rio_read */

这个函数在读取数据之前,先把缓冲区填满,然后再把数据从缓冲区填入到userbuf。

返回从内部缓冲区复制到用户缓冲区的字节数,如果遇到错误(read返回-1且errno不是EINTR),则返回-1。如果遇到文件结束(EOF),并且没有数据可读(即在尝试重填缓冲区之前rio_cnt就为0),则返回0。

基于这个函数我们得到了rio_readlinebrio_readnb

rio_readlineb
/* $begin rio_readlineb */
ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
{
int n, rc;
char c, *bufp = usrbuf; for (n = 1; n < maxlen; n++) {
if ((rc = rio_read(rp, &c, 1)) == 1) {
*bufp++ = c;
if (c == '\n') {
n++;
break;
}
} else if (rc == 0) {
if (n == 1)
return 0; /* EOF, no data read */
else
break; /* EOF, some data was read */
} else
return -1; /* Error */
}
*bufp = 0;
return n-1;
}
/* $end rio_readlineb */

这段代码是一个读取行的函数,它逐个字符地从输入源(通过rio_t *rp表示)读取字符,直到遇到换行符\n或达到最大长度maxlen。这个函数在读取到行的末尾或遇到文件结束(EOF)时停止读取,并且在遇到错误时返回-1。

在函数的最后,*bufp = 0;这行代码的作用是在字符串的末尾添加一个空字符(null terminator,值为0的字符),将其转换为一个标准的C字符串。C字符串是以空字符结尾的字符数组,这样做可以确保使用字符串的函数(如printfstrlen等)能够正确识别字符串的结束位置。

rio_readnb
/* $begin rio_readnb */
ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
{
size_t nleft = n;
ssize_t nread;
char *bufp = usrbuf; while (nleft > 0) {
if ((nread = rio_read(rp, bufp, nleft)) < 0)
return -1; /* errno set by read() */
else if (nread == 0)
break; /* EOF */
nleft -= nread;
bufp += nread;
}
return (n - nleft); /* return >= 0 */
}
/* $end rio_readnb */

如果只看返回值的话,这个函数似乎和rio_read()的功能似乎是一样的,都是调用者给定一个缓冲区(包含文件描述符),一个userbuf,一个最大字节数n,返回未读取的字节数。其实这两个函数的主要区别在于数据的读取量不同,前者更加侧重于单次读取操作,往往小于用户请求的数据,后者则是用户使用时才调用的函数,它会持续调用rio_read直到达到用户请求的字节数n或遇到EOF。

综上这三者的关系如下:

graph TD;
A[rio_read,底层带缓冲区的read函数] --> B(rio_readlineb);
A --> C(rio_readnb);
B <--> D(基于rio_read编写的读取一行数据);
C <--> E(基于rio_read编写的读取用户需求量的数据);

标准I/O

这里主要介绍了标准I/O在网络应用上的限制,大概意思就是两个流不能同时参与一个文件的读写。这会对套接字编程带来一些坏的影响。

回答之前的问题

说了这么多,在文件和内存之间的一个添加一个应用级缓冲,有什么好处?

在我看来主要是性能

  1. 减少磁盘I/O操作次数

    缓冲区能够一次读很多的数据,也就是将很多小的I/O操作合并成一次大的I/O操作,从而避免了程序陷入内核引发多次的磁盘读写。

  2. 减少系统调用

    每次进行文件I/O操作时,通常涉及到系统调用,这些调用在用户空间和内核空间之间进行上下文切换,有一定的开销。通过缓冲,可以减少需要进行的系统调用的次数,因为数据可以在用户空间的缓冲区中累积到一定量后再一次性地进行系统调用处理。

Chapter11 网络编程

这一章的前面介绍了一些计网的知识,大致描述了主机A中的一个进程是怎么把数据传送到主机B的进程的。

接下来我主要介绍一下套接字编程的基本概念:

一个连接是由它两端的套接字地址唯一确定的。这个套接字地址叫做套接字对,由下列元组来表示:

(cliaddr:cliport, servaddr:servport)

其中 cliaddr 是客户端的 IP 地址,cliport是客户端的端口,servaddr是服务器的IP地址,而servport 是服务器的端口。例如,图11-11 展示了一个 Web 客户端和一个 Web

服务器之间的连接。

套接字接口

套接字地址结构

主机和服务的转换

Linux提供了一些强大的函数(称为 getaddrinfogetnameinfo)实现二进制套接字地址结构和主机名、主机地址、服务名和端口号的字符串表示之间的相互转化。当和套接字接

口一起使用时,这些函数能使我们编写独立于任何特定版本的IP协议的网络程序。

getaddrinfo

这个函数接收一个主机地址host,服务类型service,以及一个控制连接属性的hints,返回一个指向addrinfo结构的列表。

在客户端调用了 getaddrinfo之后,会遍历这个列表,依次尝试每个套接字地址,直到调 用socket 和 connect 成功,建立起连接。类似地,服务器会尝试遍历列表中的每个套接字地址,直到调用 socket 和 bind成功,描述符会被绑定到一个合法的套接字地址。为了避免内存泄漏,应用程序必须在最后调用 freeaddrinfo,释放该链表。

getnameinfo

这个函数接受一个套接字地址,套接字的大小。然后将套接字中的主机地址和服务类型保存到host和service中。

至于flags:

示例

我们看一段代码 hostinfo.c,综合运用了上面的两个函数,这段代码展示出域名到它相关联的IP地址之间的映射:

/* $begin hostinfo */
#include "csapp.h" int main(int argc, char **argv)
{
struct addrinfo *p, *listp, hints;
char buf[MAXLINE];
int rc, flags; if (argc != 2) {
fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
exit(0);
} /* Get a list of addrinfo records */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* IPv4 only */ //line:netp:hostinfo:family
hints.ai_socktype = SOCK_STREAM; /* Connections only */ //line:netp:hostinfo:socktype
if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
exit(1);
} /* Walk the list and display each IP address */
flags = NI_NUMERICHOST; /* Display address string instead of domain name */
for (p = listp; p; p = p->ai_next) {
Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
printf("%s\n", buf);
} /* Clean up */
Freeaddrinfo(listp); exit(0);
}
/* $end hostinfo */

我们可以看到这段代码先是hints定义了连接的一些属性,然后调用getaddrinfo获得与给定服务器的连接列表,再去遍历这个addrinfo列表,读取其中的ip。

这里也可以用这个ip访问百度了。

进一步封装

这一小节介绍客户端使用getaddrinfo和socket函数得到和服务器连接的函数openclientfd以及服务器创建监听描述符的函数opend_listenfd。

/********************************
* Client/server helper functions
********************************/
/*
* open_clientfd - Open connection to server at <hostname, port> and
* return a socket descriptor ready for reading and writing. This
* function is reentrant and protocol-independent.
*
* On error, returns:
* -2 for getaddrinfo error
* -1 with errno set for other errors.
*/
/* $begin open_clientfd */
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p; /* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
} /* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */ /* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0) { /* Connect failed, try another */ //line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
} /* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
/* $end open_clientfd */ /*
* open_listenfd - Open and return a listening socket on port. This
* function is reentrant and protocol-independent.
*
* On error, returns:
* -2 for getaddrinfo error
* -1 with errno set for other errors.
*/
/* $begin open_listenfd */
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1; /* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
} /* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */ /* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int)); /* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0) { /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
} /* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1; /* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
/* $end open_listenfd */

一个用于得到一个文件描述符clientfd,客户端可以直接通过这个描述符进行文件读写;一个用于打开一个监听端口,使得用户能够请求这个端口并连接。

echo服务器

这里算是对上面内容的一个综合运用,包含了系统级I/O。

客户端

/*
* echoclient.c - An echo client
*/
/* $begin echoclientmain */
#include "csapp.h" int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio; if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2]; clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd); while (Fgets(buf, MAXLINE, stdin) != NULL) {
Rio_writen(clientfd, buf, strlen(buf)); //将读取的文本行发送给服务器
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd); //line:netp:echoclient:close
exit(0);
}
/* $end echoclientmain */

客户端使用服务器的地址和端口,得到一个连接clientfd,这个时候使用Rio_writen将buf中的数据写到clientfd,其实也就是将buf中的数据传送到了服务器。

然后再使用Rio_readlineb读取服务器传回的数据,打印输出。

服务器

/*
* echoserveri.c - An iterative echo server
*/
/* $begin echoserverimain */
#include "csapp.h" /*
* echo - read and echo text lines until client closes connection
*/
/* $begin echo */
#include "csapp.h" void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
char test[6] = {'h', 'e', 'l', 'l', 'o', '\n'};
rio_t rio; Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) { //line:netp:echo:eof
printf("server received %d bytes\n", (int)n);
Fputs(buf, stdout);
Rio_writen(connfd, test, 6);
}
}
/* $end echo */ int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; /* Enough space for any address */ //line:netp:echoserveri:sockaddrstorage
char client_hostname[MAXLINE], client_port[MAXLINE]; if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
} listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}
exit(0);
}
/* $end echoserverimain */

服务器端则是先使用listenfd = Open_listenfd(argv[1]);打开一个监听端口,然后进入一个无限循环,调用Accept函数等待来自客户端的连接,连接之后输出客户端的信息,然后调用echo给客户端返回信息。

echo服务器总结

其实我一开始看客户端和服务器端的代码的时候,是有很多疑问的,惊讶于为什么服务器和客户端运行的如此有序

服务器端接受客户端信息并打印:

客户端发送给服务器信息并打印接受到的信息:

其中红框是发送的信息,hello是服务器返回的信息;

一方面我好奇为什么运行地如此有序?客户端发送数据,服务器接受数据,然后服务器再返回数据,客户端再打印服务器返回的数据。另一方面我由注意到,Rio_readlineb的源码如果没有读到数据,会返回-1,或者0,如果客户端读取的时候,服务器还没有返回数据,这个函数不就报错吗?然而事实并没有。

答案来自rio_readlineb里的rio_read函数里的Unix I/O 的read函数:

Unix系统调用已经帮我们实现了在网络套接字编程时,read函数没有数据读取时的阻塞行为。根据函数的包装,也就是如下:

这样前面为什么有序的问题也就解答了。

Unix I/O & 网络编程总结

本篇博文介绍了《深入理解计算机系统中》Unix I/O,以及网络编程章节的一些概念,之所以合在一起介绍是因为Unix I/O在网络编程中会用到。同时我们观察到此时的echo服务器统一时刻只能处理一个客户端的连接。下一章的并行编程我们会对echo服务器进行拓展,基于并发的理论使其能够同时处理多个连接。

CSAPP学习笔记——Chapter10,11 系统级I/O与网络编程的更多相关文章

  1. Linux学习笔记(11)linux网络管理与配置之一——配置路由与默认网关,双网卡绑定(5-6)

    Linux学习笔记(11)linux网络管理与配置之一——配置路由与默认网关,双网卡绑定(5-6) 大纲目录 0.常用linux基础网络命令 1.配置主机名 2.配置网卡信息与IP地址 3.配置DNS ...

  2. CSAPP学习笔记—虚拟内存

    CSAPP学习笔记—虚拟内存 符号说明 虚拟内存地址寻址 图9-12展示了MMU如何利用页表来实现这种映射.CPU中的一个控制寄存器,页表基址寄存器(Page Table Base Register, ...

  3. SpringBoot学习笔记(11):使用WebSocket构建交互式Web应用程序

    SpringBoot学习笔记(11):使用WebSocket构建交互式Web应用程序 快速开始 本指南将引导您完成创建“hello world”应用程序的过程,该应用程序在浏览器和服务器之间来回发送消 ...

  4. Flutter学习笔记(11)--文本组件、图标及按钮组件

    如需转载,请注明出处:Flutter学习笔记(10)--容器组件.图片组件 文本组件 文本组件(text)负责显示文本和定义显示样式,下表为text常见属性 Text组件属性及描述 属性名 类型 默认 ...

  5. javaSE学习笔记(11)--- Map

    javaSE学习笔记(11)--- Map 1.Map集合 现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射.Jav ...

  6. ROS进阶学习笔记(11)- Turtlebot Navigation and SLAM - ROSMapModify - ROS地图修改

    ROS进阶学习笔记(11)- Turtlebot Navigation and SLAM - 2 - MapModify地图修改 We can use gmapping model to genera ...

  7. Linux学习笔记(七) 查询系统

    1.查看命令 (1)man 可以使用 man 命令名称 命令查看某个命令的详细用法,其显示的内容如下: NAME:命令名称 SYNOPSIS:语法 DESCRIPTION:说明 OPTIONS:选项 ...

  8. UFLDL深度学习笔记 (四)用于分类的深度网络

    UFLDL深度学习笔记 (四)用于分类的深度网络 1. 主要思路 本文要讨论的"UFLDL 建立分类用深度网络"基本原理基于前2节的softmax回归和 无监督特征学习,区别在于使 ...

  9. Linux学习笔记(三):系统执行级与执行级的切换

    1.Linux系统与其它的操作系统不同,它设有执行级别.该执行级指定操作系统所处的状态.Linux系统在不论什么时候都执行于某个执行级上,且在不同的执行级上执行的程序和服务都不同,所要完毕的工作和所要 ...

  10. [CSAPP笔记][第十章 系统级I/O]

    第十章 系统级I/O 输入/输出(I/O) : 是指主存和外部设备(如磁盘,终端,网络)之间拷贝数据过程. 高级别I/O函数 scanf和printf <<和>> 使用系统级I ...

随机推荐

  1. 推荐 5 个 火火火火 的CMS开源.Net项目

    下面推荐5个基于.NetCore开发的CMS开源项目. 一.OrchardCore 基于ASP.NET Core 构建的.模块化和多租户应用程序框架,采用文档数据库,非常高性能,跨平台的系统. 1.跨 ...

  2. 即时通讯技术文集(第36期):《跟着源码学IM》系列专题 [共12篇]

    为了更好地分类阅读 52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第36 期. [-1-] 跟着源码学IM(一):手把手教你用Netty实现心跳机制.断线重连机制 ...

  3. 即时通讯技术文集(第10期):IM通信协议该选TCP还是UDP [共12篇]

    为了更好地分类阅读52im.net 总计1000多篇精编文章,我将在每周三推送新的一期技术文集,本次是第10 期. [-1-] 简述传输层协议TCP和UDP的区别 [链接] http://www.52 ...

  4. 阿里IM技术分享(九):深度揭密RocketMQ在钉钉IM系统中的应用实践

    本文由钉钉技术专家尹启绣分享,有修订和重新排版. 1.引言 短短的几年时间,钉钉便迅速成为一款国民级应用,发展速度堪称迅猛. IM作为钉钉最核心的功能,每天需要支持海量企业用户的沟通,同时还通过 Pa ...

  5. KES(KingBaseES)集群部署实战

    今天我们将探讨关于KES(KingBaseES)的集群部署方案.作为示例,我们将以读写分离(RWC)集群为例,快速在本地进行部署和安装,并深入了解KES的部署流程.在本章中,我们将采用Windows平 ...

  6. 一个小的图文编辑软件 -- 采用winform开发

    本人用winform开发了一款图文编辑软件,实现了图片.文字.图形混合排版; 可以对图元调整大小.设置角度.添加剪切区间等操作.本人以前也写过一款类似的软件<WinForm版图像编辑小程序> ...

  7. 《C++并发编程实战》读书笔记(3):并发操作的同步

    1.条件变量 当线程需要等待特定事件发生.或是某个条件成立时,可以使用条件变量std::condition_variable,它在标准库头文件<condition_variable>内声明 ...

  8. springboot之结合mybatis增删改查解析

    1. 场景描述 本节结合springboot2.springmvc.mybatis.swagger2等,搭建一个完整的增删改查项目,希望通过这个基础项目,能帮忙朋友快速上手springboot2项目. ...

  9. Java内存模型深度剖析

    为什么要有内存模型  在介绍Java内存模型之前,先来看一下到底什么是计算机内存模型,然后再来看Java内存模型在计算机内存模型的基础上做了哪些事情.要说计算机的内存模型,就要说一下一段古老的历史,看 ...

  10. c++:-6

    上一节学习了C++中的多态性:c++:-5,本节学习C++的函数模版.数据结构以及排序查找操作: 模版 函数模版 思考:如果重载的函数,其解决问题的逻辑是一致的.函数体语句相同,只是处理的数据类型不同 ...