1.socket套接字:
概念:
局域网和广域网
局域网 :局域网将一定区域内的各种计算机、外部设备和数据库连接起来形成计算机通信的私有网络。
广域网 :又称广域网、外网、公网。是连接不同地区局域网或城域网计算机 通信的远程公共网络。
IP (Internet Protocol):本质是一个整形数,用于表示计算机在网络中的地址。IP 协议版本有两个:IPv4 和 IPv6
IPv4 (Internet Protocol version4):
使用一个 32 位的整形数描述一个 IP 地址,4 个字节,int 型
也可以使用一个点分十进制字符串描述这个 IP 地址: 192.168.247.135
分成了 4 份,每份 1 字节,8bit(char),最大值为 255
0.0.0.0 是最小的 IP 地址
255.255.255.255 是最大的 IP 地址
按照 IPv4 协议计算,可以使用的 IP 地址共有 2^32^ 个
IPv6 (Internet Protocol version6):
使用一个 128 位的整形数描述一个 IP 地址,16 个字节
也可以使用一个字符串描述这个 IP 地址:2001:0db8:3c4d:0015:0000:0000:1a2f:1a2b
分成了 8 份,每份 2 字节,每一部分以 16 进制的方式表示
按照 IPv6 协议计算,可以使用的 IP 地址共有 2^128^ 个
查看IP地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # linux $ ifconfig # windows $ ipconfig # 测试网络是否畅通 # 主机a: 192.168.1.11 # 当前主机: 192.168.1.12 $ ping 192.168.1.11 $ ping www.baidu.com # 特殊的IP地址: 127.0.0.1 ==> 和本地的IP地址是等价的 # 假设当前电脑没有联网, 就没有IP地址, 又要做网络测试, 可用使用 127.0.0.1 进行本地测试
端口的作用是定位到主机上的某一个进程,通过这个端口进程就可以接受到对应的网络数据了。
比如: 在电脑上运行了微信和QQ, 小明通过客户端给我的的微信发消息, 电脑上的微信就收到了消息, 为什么?
运行在电脑上的微信和QQ都绑定了不同的端口
通过IP地址可以定位到某一台主机,通过端口就可以定位到主机上的某一个进程
通过指定的IP和端口,发送数据的时候对端就能接受到数据了
端口也是一个整形数 unsigned short ,一个 16 位整形数,有效端口的取值范围是:0 ~ 65535 (0 ~ 2^16^-1)
提问:计算机中所有的进程都需要关联一个端口吗,一个端口可以被重复使用吗?
不需要,如果这个进程不需要网络通信,那么这个进程就不需要绑定端口的
一个端口只能给某一个进程使用,多个进程不能同时使用同一个端口
OSI/ISO 网络(七层)分层模型
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织组织)在1985年研究的网络互联模型。
物理层:负责最后将信息编码成电流脉冲或其它信号用于网上传输
数据链路层:
数据链路层通过物理网络链路供数据传输。
规定了0和1的分包形式,确定了网络数据包的形式;
网络层
网络层负责在源和终点之间建立连接;
此处需要确定计算机的位置,通过IPv4,IPv6格式的IP地址来找到对应的主机
传输层
传输层向高层提供可靠的端到端的网络数据流服务。
每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信
会话层
会话层建立、管理和终止表示层与实体之间的通信会话;
建立一个连接(自动的手机信息、自动的网络寻址);
表示层:
对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
应用层:
tcp/ip 四层模型:
应用层:对应iso/osi的应用层、表示层、会话层
传输层:对应iso/osi的传输层
网络互联层: 对应iso/osi的网络互联层
网络接口层:对应iso/osi数据链路层、物理层
网络协议
网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。一般系统网络协议包括五个部分:通信环境,传输服务,词汇表,信息的编码格式,时序、规则和过程。下面是常用的协议格式:
TCP 协议 -> 传输层协议:
UDP 协议 -> 传输层协议:
IP 协议 -> 网络层协议 :
以太网帧协议 -> 网络接口层协议 :
数据的封装:
在网络通信的时候, 程序猿需要负责的应用层数据的处理(最上层)
socket编程
Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。
套接字对应程序猿来说就是一套网络通信的接口,使用这套接口就可以完成网络通信。网络通信的主体主要分为两部分:客户端和服务器端。在客户端和服务器通信的时候需要频繁提到三个概念:IP、端口、通信数据,下面介绍一下需要注意的一些细节问题。
字节序
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,也就是说对于单字符来说是没有字节序问题的,字符串是单字符的集合,因此字符串也没有字节序问题。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian ,下面先从字节序说起。
Little-Endian -> 主机字节序 (小端)
数据的低 位字节存储到内存的低 地址位 , 数据的高 位字节存储到内存的高 地址位
我们使用的 PC 机 ,数据的存储默认使用的是小端
Big-Endian -> 网络字节序 (大端)
据的低位字节 存储到内存的高地址位 , 数据的高位字节 存储到内存的低地址位
套接字通信 过程中操作的数据都是大端存储 的,包括:接收/发送的数据、IP地址、端口。
下面给一个例子:
1 2 3 4 5 6 7 8 9 / 有一个16 进制的数, 有32 位 (int ): 0xab5c01ff 内存低地址位 内存的高地址位 ---------------------------------------------------------------------------> 小端: 0xff 0x01 0x5c 0xab 大端: 0xab 0x5c 0x01 0xff
函数:
BSD Socket 提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 #include <arpa/inet.h> uint16_t htons (uint16_t hostshort) ; uint32_t htonl (uint32_t hostlong) ; uint16_t ntohs (uint16_t netshort) uint32_t ntohl (uint32_t netlong) ;
IP 地址转换
虽然 IP 地址本质是一个整形数,但是在使用的过程中都是通过一个字符串来描述,下面的函数描述了如何将一个字符串类型的 IP 地址进行大小端转换:
1 2 3 int inet_pton (int af, const char *src, void *dst) ;
参数:
af: 地址族 (IP 地址的家族包括 ipv4 和 ipv6) 协议
AF_INET: ipv4 格式的 ip 地址
AF_INET6: ipv6 格式的 ip 地址
src: 传入参数,对应要转换的点分十进制的 ip 地址: 192.168.1.100
dst: 传出参数,函数调用完成,转换得到的大端整形 IP 被写入到这块内存中
返回值:成功返回 1,失败返回 0 或者 - 1
1 2 3 #include <arpa/inet.h> const char *inet_ntop (int af, const void *src, char *dst, socklen_t size) ;
af: 地址族 (IP 地址的家族包括 ipv4 和 ipv6) 协议
AF_INET: ipv4 格式的 ip 地址
AF_INET6: ipv6 格式的 ip 地址
src: 传入参数,这个指针指向的内存中存储了大端的整形IP地址
dst: 传出参数,存储转换得到的小端的点分十进制的IP地址
size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
返回值:成功返回 1,失败返回 0 或者 - 1
成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
失败: NULL
还有一组函数也能进程 IP 地址大小端的转换,但是只能处理 ipv4 的 ip 地址:
1 2 3 4 5 in_addr_t inet_addr (const char *cp) ;char * inet_ntoa (struct in_addr in) ;
sockaddr数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 struct sockaddr { sa_family_t sa_family; char sa_data[14 ]; } typedef unsigned short uint16_t ;typedef unsigned int uint32_t ;typedef uint16_t in_port_t ;typedef uint32_t in_addr_t ;typedef unsigned short int sa_family_t ;#define __SOCKADDR_COMMON_SIZE (sizeof (unsigned short int)) struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { sa_family_t sin_family; in_port_t sin_port; struct in_addr sin_addr ; unsigned char sin_zero[sizeof (struct sockaddr) - sizeof (sin_family) - sizeof (in_port_t ) - sizeof (struct in_addr)]; }; 通常用 struct clientaddr { int client_fd; struct sockaddr_in client_addr //储存从accept 反馈来的客户端ip 信息 }//该结构体通常用于每个子线程
套接字函数
使用套接字通信函数需要包含头文件 <arpa/inet.h>(Linux下),如果包含了这个头文件, <sys/socket.h> 就不用在包含了。
1.监听套接字:
1 2 int socket (int domain, int type, int protocol) ;
参数:
domain: 使用的地址族协议
AF_INET: 使用 IPv4 格式的 ip 地址
AF_INET6: 使用 IPv4 格式的 ip 地址
type:
SOCK_STREAM: 使用流式的传输协议
SOCK_DGRAM: 使用报式 (报文) 的传输协议
protocol: 一般写 0 即可,使用默认的协议
SOCK_STREAM: 流式传输默认使用的是 tcp
SOCK_DGRAM: 报式传输默认使用的 udp、
SOCK_RAW: 原始套接字
返回值:
成功:可用于套接字通信的文件描述符
失败: -1
函数的返回值是一个文件描述符,通过这个文件描述符可以操作内核中的某一块内存,网络通信是基于这个文件描述符来完成的。
2.绑定IP和端口
1 2 int bind (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数:
sockfd: 监听的文件描述符,通过 socket () 调用得到的返回值
addr: 传入参数,要绑定的 IP 和端口信息需要初始化到这个结构体中,IP和端口要转换为网络字节序
addrlen: 参数 addr 指向的内存大小,sizeof (struct sockaddr)
返回值:成功返回 0,失败返回 - 1
3.设置监听
1 2 int listen (int sockfd, int backlog) ;
参数:
sockfd: 文件描述符,可以通过调用 socket () 得到,在监听之前必须要绑定 bind ()
backlog: 同时能处理的最大连接要求,最大值为 128
返回值:函数调用成功返回 0,调用失败返回 -1
4.通信套接字
1 2 int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen) ;
参数:
sockfd: 监听的文件描述符
addr: 传出参数,里边存储了建立连接的客户端的地址信息
addrlen: 传入传出参数,用于存储 addr 指向的内存大小
返回值:函数调用成功,得到一个文件描述符,用于和建立连接的这个客户端通信,调用失败返回 -1
主:当客户端信息(IP,端口)不被要求知晓时,通常后两个参数为nullptr
accept函数是一个阻塞函数,当没有新的客户端连接请求的时候,该函数阻塞;当检测到有新的客户端连接请求时,阻塞解除,新连接就建立了,得到的返回值也是一个文件描述符,基于这个文件描述符就可以和客户端通信了。
5.数据接受和传递函数(需要包含头文件unistd.h):
1 2 3 4 5 6 ssize_t read (int sock_fd, void *buf, size_t size) ;ssize_t write (int sock_fd, const void *buf, size_t len) ;ssize_t recv (int sock_fd, void *buf, size_t size, int flags) ;ssize_t send (int sock_fd, const void *buf, size_t len, int flags) ;
参数:
sockfd: 用于通信的文件描述符,accept () 函数的返回值
buf: 指向一块有效内存,用于存储接收是数据
size: 参数 buf 指向的内存的容量
flags: 特殊的属性,一般不使用,指定为 0
返回值:
大于 0:实际接收的字节数
等于 0:客户端/服务端断开了连接
-1:接收数据失败了
如果连接没有断开,接收端接收不到数据,接收数据的函数会阻塞等待数据到达,数据到达后函数解除阻塞,开始接收数据,当发送端断开连接,接收端无法接收到任何数据,但是这时候就不会阻塞了,函数直接返回0。
1 2 3 int connect (int sockfd, const struct sockaddr *addr, socklen_t addrlen) ;
参数:
sockfd: 通信的文件描述符,通过调用 socket () 函数就得到了
addr: 存储了要连接的服务器端的地址信息: iP 和 端口,这个 IP 和端口也需要转换为大端然后再赋值
addrlen: addr 指针指向的内存的大小 sizeof (struct sockaddr)
返回值:连接成功返回 0,连接失败返回 - 1
端口复用(setsockopt)
端口复用最常用的用途应该是防止服务器重启时之前绑定的端口还未释放或者程序突然退出而系统没有释放端口。这种情况下如果设定了端口复用,则新启动的服务器进程可以直接绑定端口。如果没有设定端口复用,绑定会失败,提示ADDR已经在使用中——那只好等等再重试了,麻烦!
起因 : 由socket状态转换图可知, 主动关闭连接的一端都会有一个TIME_WAIT, 时间为2msl, 以确保对端收到最后一个ACK
影响 : 如果是服务器端需要主动断开连接(例如网站更新等), 那么再次重启则需要浪费2msl时间, 为了减少这种时间开销, 因此有了端口复用
作用 : 可以立马重启服务器, 而不必等待2msl, 且不会在bind的时候发生端口被占用的错误.
函数原型+实例:
1 2 3 4 5 6 7 8 int setsockopt (int sockFd, int level, int optname, const void *optval, socklen_t optlen) ;int opt;if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, (const void *) &opt, sizeof (opt))) { perror("setsockopt" ); return -1 ; }
1 - 注意:在所有TCP服务器中,在调用bind之前设置端口复用
TCP通信流程
TCP 是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
安全:tcp 通信过程中,会对发送的每一数据包都会进行校验,如果发现数据丢失,会自动重传
流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
服务器端通信流程:
1.创建用于监听的套接字,这个套接字是一个文件描述符
2.将得到的监听的文件描述符和本地的 IP 端口进行绑定
3.设置监听 (成功之后开始监听,监听的是客户端的连接)
4.等待并接受客户端的连接请求,建立新的连接,会得到一个新的文件描述符 (通信的),没有新连接请求就阻塞
1 2 3 4 read(); / recv(); write(); / send();
6.断开连接,关闭套接字(close()函数需要包含unistd.h头文件)
在 tcp 的服务器端,有两类文件描述符
监听的文件描述符
只需要有一个
不负责和客户端通信,负责检测客户端的连接请求,检测到之后调用 accept 就可以建立新的连接
通信的文件描述符
负责和建立连接的客户端通信
如果有 N 个客户端和服务器建立了新的连接,通信的文件描述符就有 N 个,每个客户端和服务器都对应一个通信的文件描述符
文件描述符对应的内存结构:
个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
监听的文件描述符(socket_fd):
通信的文件描述符(correspondence_fd):
客户端和服务器端都有通信 的文件描述符
发送数据:调用函数 write () /send (),数据进入到内核中
接收数据:调用的函数 read () /recv (), 从内核读数据
客户端的通信流程:
2.连接服务器,需要知道服务器绑定的 IP 和端口
1 2 3 4 read(); / recv(); write(); / send();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int lfd = socket(AF_INET, SOCK_STREAM, 0 ); if (lfd == -1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(10000 ); addr.sin_addr.s_addr = INADDR_ANY; int ret = bind(lfd, (struct sockaddr*)&addr, sizeof (addr)); if (ret == -1 ) { perror("bind" ); exit (0 ); } ret = listen(lfd, 128 ); if (ret == -1 ) { perror("listen" ); exit (0 ); } struct sockaddr_in cliaddr ; int clilen = sizeof (cliaddr); int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen); if (cfd == -1 ) { perror("accept" ); exit (0 ); } char ip[24 ] = {0 }; printf ("客户端的IP地址: %s, 端口: %d\n" , inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof (ip)), ntohs(cliaddr.sin_port)); while (1 ) { char buf[1024 ]; memset (buf, 0 , sizeof (buf)); int len = read(cfd, buf, sizeof (buf)); if (len > 0 ) { printf ("客户端say: %s\n" , buf); write(cfd, buf, len); } else if (len == 0 ) { printf ("客户端断开了连接...\n" ); break ; } else { perror("read" ); break ; } } close(cfd); close(lfd); return 0 ; }
客户端的通信流程
在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符
创建一个通信的套接字
连接服务器, 需要知道服务器绑定的IP和端口(通过指定的ip和端口就可以连接到指定的服务器)
通信
1 2 3 4 read(); / recv(); write(); / send();
断开连接, 关闭文件描述符(套接字)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <arpa/inet.h> int main () { int fd = socket(AF_INET, SOCK_STREAM, 0 ); if (fd == -1 ) { perror("socket" ); exit (0 ); } struct sockaddr_in addr ; addr.sin_family = AF_INET; addr.sin_port = htons(10000 ); inet_pton(AF_INET, "192.168.237.131" , &addr.sin_addr.s_addr); int ret = connect(fd, (struct sockaddr*)&addr, sizeof (addr)); if (ret == -1 ) { perror("connect" ); exit (0 ); } int number = 0 ; while (1 ) { char buf[1024 ]; sprintf (buf, "你好, 服务器...%d\n" , number++); write(fd, buf, strlen (buf)+1 ); memset (buf, 0 , sizeof (buf)); int len = read(fd, buf, sizeof (buf)); if (len > 0 ) { printf ("服务器say: %s\n" , buf); } else if (len == 0 ) { printf ("服务器断开了连接...\n" ); break ; } else { perror("read" ); break ; } sleep(1 ); } close(fd); return 0 ; }
2.一个简单的键值数据库:
习题和源码
题目 main.cpp ClassTcp.h ClassTcp.cpp ClassMessage.h ClassMessage.cpp 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 ASYNC CPP GROUP FINAL PROJECT (2022 ) :Simple Key Value Database 在这个项目中,你们要实现一个简单的键值数据库。 架构如图所示: 要求 你们要编写database服务端,从文件中读取和写入数据,在这个项目中,总数据量不会大于内存容量。 你们需要实现以下操作: Put (Key,Value) - 将key value保存到数据库中。Delete (Key) - 将指定key删除。Get (Key) - 返回指定Key的value。你们只需要编写服务端,我们会用编写好的Client测试你的程序,所以请注意你们必须按照指定协议来与Clien 通信。 我们会使用多个连接向你的datbase发送请求,所以为你的数据结构添加线程安全的特性是必须的,但我们不会 发出有写写冲突或读写冲突的请求。 协议格式: Header: Field Length Type Meaning MagicNumber 4 bytes UInt32 Size 4 bytes UInt32 Type 4 bytes UInt32 Padding 4 bytes UInt32 Types: Number Meaning 0 Put请求 1 Delete请求 2 Get请求 3 Put响应 4 Delete响应 5 Get响应 Put Request Body: Field Length Type Meaning Key Size 4 bytes UInt32 Key的长度 Key N bytes Byte[] Key Value Size 4 bytes UInt32 Value的长度 Value N bytes Byte[] Value Delete Request Body: Field Length Type Meaning Key Size 4 bytes UInt32 Key的长度 Key N bytes Byte[] Key Get Request Body: Field Length Type Meaning Key Size 4 bytes UInt32 Key的长度 Key N bytes Byte[] Key Put Response Body: Field Length Type Meaning Status 1 byte Bool 是否成功 Delete Response Body: Field Length Type Meaning Status 1 byte Bool 是否成功 Get Response Body: Field Length Type Meaning Value Size 4 bytes UInt32 Value的长度 Value N bytes Byte[] Value Notes: 我们不会使用一个连接同时发送多个请求。 我们可能会使用多个连接同时发送多个请求,但这些请求之间不会有竟态条件。 你的程序所存储的Key Value对的总内存量不会超过内存总量。 我们可能会重启你的程序,请确保你确实对数据进行了持久化。 我们假定所有的通信都是可靠,因此不会出现断连的情况。 我们至多同时使用100个连接。 文中提及的所有Int均使用小端序。 稍后我们会放出Client的源码作为参考
1 2 3 4 5 6 7 8 #include "ClassTCP.h" using namespace std;TCP *TOC=new TCP; int main () { TOC->Socket_Start (); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #ifndef CLIONSERVER_CLASSTCP_H #define CLIONSERVER_CLASSTCP_H #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <cstdlib> #include <unistd.h> #include <cstring> #include <arpa/inet.h> #include <map> #include <mutex> class TCP {private : struct sockaddr_in ser_addr; int socket_fd; int bin_d; int lis; public : ~TCP (); void Message_import () ; bool Message_save () ; void Socket_Start () ; void Connect_Start () ; }; #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 #include <fstream> #include "ClassMessage.h" #include <thread> using namespace std;map<string,string>Map; mutex WriteMutexMap; using namespace std;extern TCP *TOC;void TCP::Socket_Start () { this ->socket_fd = socket (AF_INET,SOCK_STREAM,0 ); if (socket_fd==-1 ) { perror ("socket" ); exit (1 ); } ser_addr.sin_family=AF_INET; ser_addr.sin_port=htons (9999 ); ser_addr.sin_addr.s_addr=INADDR_ANY; int opt=1 ; setsockopt (socket_fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt)); this ->bin_d= bind (socket_fd,(struct sockaddr*)&ser_addr,sizeof (ser_addr)); if (bin_d==-1 ) { perror ("bind" ); exit (1 ); } this ->lis= listen (socket_fd,100 ); if (lis==-1 ) { perror ("listen" ); exit (1 ); } this ->Connect_Start (); } void working (Message message,struct Client_Message client_message) { message.client_message = client_message; while (true ) { if (message.Message_get (message) == 0 ) { break ; } } } void TCP::Connect_Start () { struct Client_Message Client; Message message; while (true ) { Client.connect_fd= accept (socket_fd, nullptr , nullptr ); if (Client.connect_fd == -1 ) { perror ("accept" ); exit (1 ); } cout << "客户端连接成功" << endl; thread t (working,message,Client) ; cout << "子线程创建成功" << endl; t.detach (); } } bool TCP::Message_save () { ofstream outFile; outFile.open ("DATA.txt" ,ios::out); if (!outFile.is_open ()) { perror ("open" ); exit (1 ); } WriteMutexMap.lock (); for (auto & it : Map) { outFile << it.first <<" " << it.second << endl; } WriteMutexMap.unlock (); outFile.close (); return true ; } void TCP::Message_import () { string key,value; ifstream inFile; inFile.open ("DATA.txt" ,ios::in); if (!inFile.is_open ()) { perror ("open" ); exit (1 ); } while (!inFile.eof ()) { inFile >> key; inFile >> value; Map.insert (pair <string,string>(key, value)); } inFile.close (); } TCP::~TCP () { delete TOC; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #ifndef CLIONSERVER_CLASSMESSAGE_H #define CLIONSERVER_CLASSMESSAGE_H #include <iostream> #include <sys/types.h> #include <sys/socket.h> #include <cstdlib> #include <unistd.h> #include <cstring> #include <arpa/inet.h> #include "ClassTCP.h" struct Client_Message { int connect_fd; struct sockaddr_in client_addr; }; class Message {private : struct { uint32_t MagicNumber; uint32_t Size; uint32_t Type; uint32_t Padding; }inf; public : Message (); int Message_get (Message message) ; void Types_Request (Message message) ; void Put_Response (Message message) ; void Delete_Response (Message message) ; void Get_Response (Message message) ; void header_send (Message message,uint32_t MagicNumber,uint32_t Size,uint32_t Type,uint32_t Padding) ; struct Client_Message client_message; }; #endif
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 #include "ClassTCP.h" #include "ClassMessage.h" using namespace std;extern map<string, string> Map;extern TCP *TOC;Message::Message () { this ->inf.MagicNumber = 0 ; this ->inf.Size = 0 ; this ->inf.Type = 0 ; this ->inf.Padding = 0 ; } int Message::Message_get (Message message) { while (true ) { size_t len, len1, len2, len3; len = read (message.client_message.connect_fd, &(this ->inf.MagicNumber), sizeof (this ->inf.MagicNumber)); len1 = read (message.client_message.connect_fd, &(this ->inf.Size), sizeof (this ->inf.MagicNumber)); len2 = read (message.client_message.connect_fd, &(this ->inf.Type), sizeof (this ->inf.MagicNumber)); len3 = read (message.client_message.connect_fd, &(this ->inf.Padding), sizeof (this ->inf.MagicNumber)); if (len == -1 || len1 == -1 || len2 == -1 || len3 == -1 ) { perror ("read" ); exit (1 ); } else if (len == 0 || len1 == 0 || len2 == 0 || len3 == 0 ) { cout << "客户端断开连接" << endl; close (message.client_message.connect_fd); return 0 ; } this ->Types_Request (message); } } void Message::Types_Request (Message message) { switch (this ->inf.Type) { case 0 : this ->Put_Response (message); break ; case 1 : TOC->Message_import (); this ->Delete_Response (message); break ; case 2 : TOC->Message_import (); this ->Get_Response (message); break ; default : break ; } } void Message::header_send (Message message, uint32_t MagicNumber, uint32_t Size, uint32_t Type, uint32_t Padding) { if (write (message.client_message.connect_fd, &MagicNumber, sizeof (MagicNumber)) == -1 ) { perror ("write" ); } if (write (message.client_message.connect_fd, &Size, sizeof (Size)) == -1 ) { perror ("write" ); } if (write (message.client_message.connect_fd, &Type, sizeof (Type)) == -1 ) { perror ("write" ); } if (write (message.client_message.connect_fd, &Padding, sizeof (Padding)) == -1 ) { perror ("write" ); } } void Message::Put_Response (Message message) { string key, value; uint32_t keysize, valuesize; if (read (message.client_message.connect_fd, &keysize, sizeof (keysize)) == -1 ) { cout << "read error" << endl; exit (1 ); } key.resize (keysize); if (read (message.client_message.connect_fd, const_cast <char *>(key.data ()), keysize) == -1 ) { cout << "read error" << endl; exit (1 ); } if (read (message.client_message.connect_fd, &valuesize, sizeof (valuesize)) == -1 ) { cout << "read error" << endl; exit (1 ); } value.resize (valuesize); if (read (message.client_message.connect_fd, const_cast <char *>(value.data ()), valuesize) ==-1 ) { cout << "read error" << endl; exit (1 ); } Map[key] = value; bool result = TOC->Message_save (); uint32_t MagicNumber = 1234 ; uint32_t Size = sizeof (bool ); uint32_t Type = 3 ; uint32_t Padding = 0 ; this ->header_send (message, MagicNumber, Size, Type, Padding); if (write (message.client_message.connect_fd, &result, sizeof (result)) == -1 ) { perror ("write" ); } } void Message::Delete_Response (Message message) { string key; uint32_t keysize; if (read (message.client_message.connect_fd, &keysize, sizeof (keysize)) == -1 ) { perror ("read" ); exit (1 ); } key.resize (keysize); if (read (message.client_message.connect_fd, const_cast <char *>(key.data ()), keysize) == -1 ) { perror ("read" ); exit (1 ); } Map.erase (key); bool result = TOC->Message_save (); uint32_t MagicNumber = 1234 ; uint32_t Size = sizeof (bool ); uint32_t Type = 4 ; uint32_t Padding = 0 ; this ->header_send (message, MagicNumber, Size, Type, Padding); if (write (message.client_message.connect_fd, &result, sizeof (result)) == -1 ) { perror ("write" ); } } void Message::Get_Response (Message message) { string key; uint32_t keysize; if (read (message.client_message.connect_fd, &keysize, sizeof (keysize)) == -1 ) { perror ("read" ); exit (1 ); } key.resize (keysize); if (read (message.client_message.connect_fd, const_cast <char *>(key.data ()), keysize) == -1 ) { perror ("read" ); exit (1 ); } auto iter = Map.find (key); string value = (iter->second); uint32_t valuesize = (iter->second).size (); uint32_t MagicNumber = 1234 ; uint32_t Size = valuesize + 4 ; uint32_t Type = 5 ; uint32_t Padding = 0 ; this ->header_send (message, MagicNumber, Size, Type, Padding); cout << value << endl; cout << valuesize << endl; if (write (message.client_message.connect_fd, &valuesize, sizeof (valuesize)) == -1 ) { perror ("write" ); exit (1 ); } cout << value << endl; if (write (message.client_message.connect_fd, const_cast <char *>(value.data ()), valuesize) == -1 ) { perror ("write" ); exit (1 ); } }
相关笔记
map
map是STL(标准模板库)的一个关联容器,它提供一对一的hash
map的功能 :
自动建立key - value的对应。key 和 value可以是任意你需要的类型,包括自定义类型。
map<type_name,type_name>
使用map
使用map得包含map类所在的头文件
#include //注意,STL头文件没有扩展名.h
map对象是模板类,需要关键字和存储对象两个模板参数:
std:map<int , string> personnel;
用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 map <int , string > mapStudent; mapStudent.insert(pair <int , string >(000 , "student_zero" )); mapStudent.insert(map <int , string >::value_type(001 , "student_one" )); mapStudent[123 ] = "student_first" ; mapStudent[456 ] = "student_second" ; mapStudent.find(key) 用auto iter = mapStudent.find(key); iter->first iter->second mapStudent.erase(key) mapStudent.clear() mapStudent.(mapStudent.begin(),mapStudent.end()); int nSize = mapStudent.size();map 的基本操作函数:更多见CSDN
注意:
以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的 插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的 ,但是用数组 方式就不同了,它可以覆盖以前该关键字对 应的值 ,用程序说明如下:
1 2 3 4 5 6 map<string,string>Map; Map["key" ] = "value" ; Map.insert (pair <string, string>("key" ,"other" )); Map["key" ] = "other" ;
补充:
map的遍历(迭代器)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 map 迭代器map <char ,string >::iterator it; map <char ,string >::const_iterator it;for (auto & iter : Map) { outFile << iter.first <<endl outFile << iter.second << endl ; }
perror函数
perror函数可以在发生错误时多输出的数据
下方为例子:
1 2 3 4 5 if (lis==-1 ) { perror("listen" ); exit (1 ); }
data()成员函数用法
标准库的string类供了3个成员函数来从一个string得到c类型的字符数组 :c_str()、data()、copy(p,n)。
1. c_str():生成一个const char*指针,指向以空字符终止的数组
2. data():生成一个const char*指针,指向的数组不以空字符结束
1 2 3 4 5 6 7 8 string key; read (message.client_message.connect_fd, const_cast <char *>(key.data (),keysize);
resize 函数
resize(),设置大小;
reserve(),设置容量;
resize()是分配容器的内存大小,而reserve()只是设置容器容量大小,但并没有真正分配内存。
resize()可以传递两个参数,分别是大小和初始值,初始值默认为0,reserve()只能传递一个参数,不能设置初始值,其初始值为系统随机生成。
例:
1 2 3 4 5 6 7 8 9 #include <vector> #include <string> using namespace std;string key1; uint32_t keysize;vector<int >key2; key1.resize (keysize); key2.resize (keysize);
多线程std::thread
头文件#include
看看CSDN全面用法:(10条消息) C++11 多线程(std::thread)详解_std::thread()是个函数吗_sjc_0910的博客-CSDN博客
(10条消息) 两种 C++ 多线程编程方式,看完不懂打我…_c++多线程_阿基米东的博客-CSDN博客
创建线程基本用法:
1 2 3 #include <thread> thread t (工作的函数,该函数参数,该函数参数,,,,,)
我的用法(具体见上方代码)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <thread> void TCP::Connect_Start () { struct Client_Message Client; Message message; while (true ) { Client.connect_fd= accept (socket_fd, nullptr , nullptr ); if (Client.connect_fd == -1 ) { perror ("accept" ); exit (1 ); } cout << "客户端连接成功" << endl; thread t (working,message,Client) ; cout << "子线程创建成功" << endl; t.detach (); } } void working (Message message,struct Client_Message client_message) { message.client_message = client_message; while (true ) { if (message.Message_get (message) == 0 ) { break ; } } }
关于我在第一次写多线程犯的致命错误:
刚开始写我竟然让所有线程共用全局的一个类!!!(这样会导致不同线程在同时读写该类数据,一定会爆炸) 血的教训!!
注意:在写子线程的工作函数时,最好是让这个thread_working函数独立(独立于类)出来,不要什么东西都一起用
这个地方后面修改为了将使用类在每个子线程中拷贝一份类(所有数据都会拷贝一份),使所有的线程单独使用不同的类,这样就避免了同时读写导致程序崩溃
还有一定要让子线程退出,不退出也会爆炸
还有别忘了断开连接时close()关闭通信套接字
std::mutex(互斥锁)
头文件:#include
std::mutex
是 C++11 中最基本的互斥量,一个线程将mutex锁住时,其它的线程就不能操作mutex,直到这个线程将mutex解锁。
一般都是创建全局锁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <mutex> #include <map> map<string,string>Map; mutex WriteMutexMap; WriteMutexMap.lock (); for (auto & it : Map) { outFile << it.first <<" " << it.second << endl; } WriteMutexMap.unlock (); outFile.close ();
常用函数:
1 2 3 4 5 6 7 8 9 10 #include <mutex> mutex WriteMutexMap; WriteMutexMap.lock (); WriteMutexMap.unlock (); WriteMutexMap.try_lock ();