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年研究的网络互联模型。

    img

    • 物理层:负责最后将信息编码成电流脉冲或其它信号用于网上传输
    • 数据链路层:
      数据链路层通过物理网络链路供数据传输。
      规定了0和1的分包形式,确定了网络数据包的形式;
    • 网络层
      网络层负责在源和终点之间建立连接;
      此处需要确定计算机的位置,通过IPv4,IPv6格式的IP地址来找到对应的主机
    • 传输层
      传输层向高层提供可靠的端到端的网络数据流服务。
      每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信
    • 会话层
      会话层建立、管理和终止表示层与实体之间的通信会话;
      建立一个连接(自动的手机信息、自动的网络寻址);
    • 表示层:
      对应用层数据编码和转化, 确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;
    • 应用层:

tcp/ip 四层模型:

  • 应用层:对应iso/osi的应用层、表示层、会话层
  • 传输层:对应iso/osi的传输层
  • 网络互联层: 对应iso/osi的网络互联层
  • 网络接口层:对应iso/osi数据链路层、物理层

网络协议

网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。一般系统网络协议包括五个部分:通信环境,传输服务,词汇表,信息的编码格式,时序、规则和过程。下面是常用的协议格式:

TCP 协议 -> 传输层协议:

img

UDP 协议 -> 传输层协议:

img

IP 协议 -> 网络层协议

img

以太网帧协议 -> 网络接口层协议

img

数据的封装:

1558001080021

在网络通信的时候, 程序猿需要负责的应用层数据的处理(最上层)

  • 应用层的数据可以使用某些协议进行封装,也可以不封装

  • 程序猿需要调用发送数据的接口函数,将数据发送出去

  • 程序猿调用的 API 做底层数据处理

  • 传输层使用传输层协议打包数据

    • 网络层使用网络层协议打包数据
    • 网络接口层使用网络接口层协议打包数据
    • 数据被发送到 internet
    • 接收端接收到发送端的数据
  • 接收端接收到发送端的数据

    • 程序猿调用接收数据的函数接收数据

    • 调用的 API 做相关的底层处理:

    • 网络接口层拆包 ==> 网络层的包

    • 网络层拆包 ==> 网络层的包

    • 传输层拆包 ==> 传输层数据

    • 如果应用层也使用了协议对数据进行了封装,数据的包的解析需要程序猿做

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
// 字节序, 最小的单位: char 字节, int 有4个字节, 需要将其拆分为4份
// 一个字节 unsigned char, 最大值是 255(十进制) ==> ff(16进制)
内存低地址位 内存的高地址位
--------------------------------------------------------------------------->
小端: 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>
// u:unsigned
// 16: 16位, 32:32位
// h: host, 主机字节序
// n: net, 网络字节序
// s: short
// l: int

// 这套api主要用于 网络通信过程中 IP 和 端口 的 转换
// 将一个短整形从主机字节序 -> 网络字节序
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
// 主机字节序的IP地址转换为网络字节序
// 主机字节序的IP地址是字符串, 网络字节序IP地址是整形
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>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
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
// 点分十进制IP -> 大端整形
in_addr_t inet_addr (const char *cp);

// 大端整形 -> 点分十进制IP
char* inet_ntoa(struct in_addr in);

sockaddr数据结构

img

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; // 地址族协议, ipv4
char sa_data[14]; // 端口(2字节) + IP地址(4字节) + 填充(8字节)
}

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;
};

// sizeof(struct sockaddr) == sizeof(struct sockaddr_in)
struct sockaddr_in
{
sa_family_t sin_family; /* 地址族协议: AF_INET */
in_port_t sin_port; /* 端口, 2字节-> 大端 */
struct in_addr sin_addr; /* IP地址, 4字节 -> 大端 */
/* 填充 8字节 */
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
// 将文件描述符和本地的IP与端口进行绑定   
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
// 成功连接服务器之后, 客户端会自动随机绑定一个端口
// 服务器端调用accept()的函数, 第二个参数存储的就是客户端的IP和端口信息
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;
}//设置端口复用
/* Returns 0 on success, -1 for errors. */
1
- 注意:在所有TCP服务器中,在调用bind之前设置端口复用

TCP通信流程

TCP 是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。

  • 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
  • 安全:tcp 通信过程中,会对发送的每一数据包都会进行校验,如果发现数据丢失,会自动重传
  • 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致

img

服务器端通信流程:

  • 1.创建用于监听的套接字,这个套接字是一个文件描述符
1
int lfd = socket();
  • 2.将得到的监听的文件描述符和本地的 IP 端口进行绑定
1
bind();
  • 3.设置监听 (成功之后开始监听,监听的是客户端的连接)
1
listen();
  • 4.等待并接受客户端的连接请求,建立新的连接,会得到一个新的文件描述符 (通信的),没有新连接请求就阻塞
1
int cfd = accept();
  • 5.通信,读写操作默认都是阻塞的
1
2
3
4
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
  • 6.断开连接,关闭套接字(close()函数需要包含unistd.h头文件)
1
close();

在 tcp 的服务器端,有两类文件描述符

  • 监听的文件描述符
    • 只需要有一个
    • 不负责和客户端通信,负责检测客户端的连接请求,检测到之后调用 accept 就可以建立新的连接
  • 通信的文件描述符
    • 负责和建立连接的客户端通信
    • 如果有 N 个客户端和服务器建立了新的连接,通信的文件描述符就有 N 个,每个客户端和服务器都对应一个通信的文件描述符
  • 文件描述符对应的内存结构:

    • 个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区

    • 读数据: 通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区

    • 写数据: 通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区

  • 监听的文件描述符(socket_fd):

    • 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中

    • 读缓冲区中有数据,说明有新的客户端连接

    • 调用 accept () 函数,这个函数会检测监听文件描述符的读缓冲区

    • 检测不到数据,该函数阻塞

    • 如果检测到数据,解除阻塞,新的连接建立

  • 通信的文件描述符(correspondence_fd):

  • 客户端和服务器端都有通信的文件描述符

  • 发送数据:调用函数 write () /send (),数据进入到内核中

    • 数据并没有被发送出去,而是将数据写入到了通信的文件描述符对应的写缓冲区中

    • 内核检测到通信的文件描述符写缓冲区中有数据,内核会将数据发送到网络中

  • 接收数据:调用的函数 read () /recv (), 从内核读数据

    • 数据如何进入到内核程序猿不需要处理,数据进入到通信的文件描述符的读缓冲区中

    • 数据进入到内核,必须使用通信的文件描述符,将数据从读缓冲区中读出即可

客户端的通信流程:

  • 1.创建一个通信的套接字
1
int cfd = socket();
  • 2.连接服务器,需要知道服务器绑定的 IP 和端口
1
connect()
  • 3.通信
1
2
3
4
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
  • 4.断开连接,关闭文件描述符 (套接字)
1
close();

基于tcp的服务器端通信代码:

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
// server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
// 1. 创建监听的套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}

// 2. 将socket()返回值和本地的IP端口绑定到一起
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(10000); // 大端端口

// INADDR_ANY代表本机的所有IP, 假设有三个网卡就有三个IP地址
// 这个宏可以代表任意一个IP地址
// 这个宏一般用于本地的绑定操作
addr.sin_addr.s_addr = INADDR_ANY; // 这个宏的值为0 == 0.0.0.0
// inet_pton(AF_INET, "192.168.237.131", &addr.sin_addr.s_addr);
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}

// 3. 设置监听
ret = listen(lfd, 128);
if(ret == -1)
{
perror("listen");
exit(0);
}

// 4. 阻塞等待并接受客户端连接
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));

// 5. 和客户端通信
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;
}

客户端的通信流程

在单线程的情况下客户端通信的文件描述符有一个, 没有监听的文件描述符

  1. 创建一个通信的套接字
1
int cfd = socket();
  1. 连接服务器, 需要知道服务器绑定的IP和端口(通过指定的ip和端口就可以连接到指定的服务器)
1
connect();
  1. 通信
1
2
3
4
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
  1. 断开连接, 关闭文件描述符(套接字)
1
close();

在基于tcp通信的客户端通信代码:

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
// client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>

int main()
{
// 1. 创建通信的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}

// 2. 连接服务器
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);
}

// 3. 和服务器端通信
int number = 0;
while(1)
{
// 发送数据,前面的fd已经是通信的套接字了
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); // 每隔1s发送一条数据
}

close(fd);

return 0;
}

2.一个简单的键值数据库:

习题和源码

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// 魔数(固定为1234)
Size 4 bytes UInt32 //包大小,不包括头部
Type 4 bytes UInt32 //包类型
Padding 4 bytes UInt32 //填充(固定为0)
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
// Created by: keqiudi
#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
//
// Created by keqiudi on 2023/2/27.
//

#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 //CLIONSERVER_CLASSTCP_H
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
//
// Created by keqiudi on 2023/2/27.
//
#include<fstream>
#include"ClassMessage.h"
#include<thread>
using namespace std;
map<string,string>Map;//全局Map容器
mutex WriteMutexMap;//创建全局锁,避免多线程对map容器的操作造成数据混乱

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;//将socket()返回值和本地的IP(IPv4)端口绑定到一起
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);//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; //拷贝一份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
//
// Created by keqiudi on 2023/2/27.
//

#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 //CLIONSERVER_CLASSMESSAGE_H

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
//
// Created by keqiudi on 2023/2/27.
//

#include"ClassTCP.h"
#include"ClassMessage.h"

using namespace std;
extern map<string, string> Map;//全局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;//Header数据存储
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);//根据Type类型进行不同的操作
}
}

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);//设置key的大小
if (read(message.client_message.connect_fd, const_cast<char *>(key.data()), keysize) ==
-1)//.data()将字符串类型转换为const char*指针,const_cast<char *>(key.data())将const char*转换为char,不能直接对字符串类型进行取地址,
{
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);//设置value的大小
if (read(message.client_message.connect_fd, const_cast<char *>(value.data()), valuesize) ==-1)//const_cast<char *>(key.data())将const char*转换为char*
{
cout << "read error" << endl;
exit(1);
}

Map[key] = value;//将数据存入map中
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);//设置key的大小
if (read(message.client_message.connect_fd, const_cast<char *>(key.data()), keysize) ==
-1)//const_cast<char *>(key.data())将const char*转换为char*
{
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);//设置key的大小
if (read(message.client_message.connect_fd, const_cast<char *>(key.data()), keysize) ==
-1)//const_cast<char *>(key.data())将const char*转换为char*
{
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

  • 第一个可以称为关键字(key),每个关键字(key)只能在map中出现一次;

  • 第二个可能称为该关键字的值(value);

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对象
map<int, string> mapStudent;

// 第一种 用insert函數插入pair
mapStudent.insert(pair<int, string>(000, "student_zero"));

// 第二种 用insert函数插入value_type数据
mapStudent.insert(map<int, string>::value_type(001, "student_one"));

// 第三种 用"array"方式插入
mapStudent[123] = "student_first";//123为key,"student_first"为value
mapStudent[456] = "student_second";


//map查找元素
mapStudent.find(key)//当所查找的关键key出现时,它返回数据所在对象的位置,否则返回map::end()位置
auto iter = mapStudent.find(key);//得到指向key的一个指针
iter->first//key的值
iter->second//value的值

//map删除元素
mapStudent.erase(key)//删除关键字,如果成功删除返回1,否则返回0
mapStudent.clear()//清空所有元素
mapStudent.(mapStudent.begin(),mapStudent.end());//与上方等同

//获取map的大小
int nSize = mapStudent.size();

map的基本操作函数:更多见CSDN

注意:

以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的 插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是不能在插入数据的,但是用数组方式就不同了,它可以覆盖以前该关键字对 应的值,用程序说明如下:

1
2
3
4
5
6
map<string,string>Map;
//以下都是string类型
Map["key"] = "value";//key与value对应

Map.insert(pair<string, string>("key","other"));//操作失败,已经存在key,insert失败,
Map["key"] = "other";//操作成功,用数组写入相同key 时,会将原来存在的value覆盖

补充:

map的遍历(迭代器)

1
2
3
4
5
6
7
8
9
10
11
12
13
14

map迭代器
map<char,string>::iterator it; //it能读写map<char,string>的元素
map<char,string>::const_iterator it;//it只能读map<char,string>的元素,不可以修改map<char,string>中的元素


for(auto & iter : Map)
{
outFile << iter.first <<endl
outFile << iter.second << endl;
}
//iter.first是关键子key的值
//iter.second是value的值
//遍历的时候按照key值遍历

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
//此处用于通信时读取数据read函数的参数

//注意通常不直接对字符串类型直接取地址!!!,string是个类,所以需要data()获取指针

string key;
read(message.client_message.connect_fd, const_cast<char *>(key.data(),keysize);

//const_cast<char *>可以将const char*转化为char*型

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);//为vector容器设置大小

多线程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;//这个地方可以直接用一个in connect_fd,下面传参的时候同样也传进去,当不用客户端数据的时候,就不用结构体了

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;//创建全局锁,避免多线程对map容器的操作造成数据混乱

//在写入数据时的简单操作:

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();//将mutex上锁。如果mutex已经被其它线程上锁,,那么会阻塞,直到解锁;如果mutex已经被同一个线程锁住,那么会产生死锁。


WriteMutexMap.unlock();//解锁mutex,释放其所有权。如果有线程因为调用lock()不能上锁而被阻塞,则调用此函数会将mutex的主动权随机交给其中一个线程;如果mutex不是被此线程上锁,那么会引发未定义的异常。

WriteMutexMap.try_lock();//尝试将mutex上锁。如果mutex未被上锁,则将其上锁并返回true;如果mutex已被锁则返回false。