一、前言:
通过套接字实现通信和语言类型无关 知识调用不同的接口
Socket上篇文章已经介绍过了 他是实现网络编程的基础 。
在服务器模型中通常都是多个客户端一个服务器端,那么服务器是如何处理多个客户端的请求?
1.顺序处理 依次处理--效率低下
2.并发处理
2.1 多线程并发(线程池)
2.2 多进程池
2.3 I/O转接接口 select epoll -可在单线程处理多任务请求 +多线程效率更高
2.4 libevent 高并发框架
若客户端内部任务较复杂,需要同时进行上传和下载 ,可建立多个连接实现不同任务。创建套接字连接池(与线程池搭配使用),每个连接处理多个流程。
数据TCP/IP传输过程
问题解惑:协议是什么?为什么要需要协议?
个人理解:在数据传输是数据通常要以二进制传输数据。接收方与发送方要保证能够接收(TCP建立连接 IP地址寻找到目标主机 )以及接受到的内容一致(HTTP FTP等类似编码解码)。所以需要制定协议完成数据的传输。
注:在应用层以下通常不需要程序员完成,均由内核完成。
数据传输存储简图
在计算机通信过程中为了使通信双方能够正常通信,正常的编译码,所以需要对传输单元(比特 字节 等)传输顺序进行规定。由此引出字节序。
二、字节序
字节序,就是字节的顺序,最小单位是字节,即大于一个字节类型的数据在内存中存放顺序。(单字符无顺序以及字符串无顺序问题;int long 有顺序问题)
在各体系计算机中通常采用两种字节存储机制:Big-Endian和Little-Endian,即 大小端
2.1 Little-Endian -> 主机字节序 (小端) --低低 高高
据的低位字节存储到内存的低地址位, 数据的高位字节存储到内存的高地址位
我们使用的PC机,数据的存储默认使用的是小端
2.2 Big-Endian -> 网络字节序 (大端)--低高 高低
数据的低位字节存储到内存的高地址位, 数据的高位字节存储到内存的低地址位
套接字通信过程中操作的数据都是大端存储的,包括:接收/发送的数据、IP地址、端口。
在主机 通过网络传输过程中
主机发送数据: 小端转为大端 接受数据:将大端转为小端 存储
问:0x12 34 56 78 占多少个字节?
答:0xff=255 占一个字节 小于0xff均为一个字节存储, 那么0x12 0x34 0x56 0x78 各占一个字节 一共占四个字节
内存对齐
问:0x12 34 56 78 大端小端各自如何存储?
答 0x12 34 56 78 为由高位字节到低位字节 因此
大端 78 56 34 12
小端 12 34 56 78
转换函数
#include <arpa/inet.h>
// 将大端的整形数, 转换为小端的点分十进制的IP地址
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 参数:
- af: 地址族协议
- AF_INET: ipv4格式的ip地址
- AF_INET6: ipv6格式的ip地址
- src: 传入参数, 这个指针指向的内存中存储了大端的整形IP地址
- dst: 传出参数, 存储转换得到的小端的点分十进制的IP地址
- size: 修饰dst参数的, 标记dst指向的内存中最多可以存储多少个字节
- 返回值:
- 成功: 指针指向第三个参数对应的内存地址, 通过返回值也可以直接取出转换得到的IP字符串
- 失败: NULL
三、TCP通信
TCP是一个面向连接的,安全的,流式传输协议,这个协议是一个传输层协议。
- 面向连接:是一个双向连接,通过三次握手完成,断开连接需要通过四次挥手完成。
- 安全:tcp通信过程中,会对发送的每一数据包都会进行校验, 如果发现数据丢失, 会自动重传
- 流式传输:发送端和接收端处理数据的速度,数据的量都可以不一致
- (三次握手 四次挥手 标志位 拥塞窗口 滑动窗口)
- TCP通信过程 API
0.文件描述符
服务器端 两个文件描述䦹 一个用于监听 一个用于 通信
客户端 只有一个 用于通信
一个文件文件描述符对应两块内存, 一块内存是读缓冲区, 一块内存是写缓冲区
- 读数据:
通过文件描述符将内存中的数据读出, 这块内存称之为读缓冲区
- 写数据:
通过文件描述符将数据写入到某块内存中, 这块内存称之为写缓冲区
0.1.监听的文件描述符:
- 客户端的连接请求会发送到服务器端监听的文件描述符的读缓冲区中
0.2通信的文件描述符:
发送数据:调用函数 write() / send(),数据进入到内核中
接收数据: 调用的函数 read() / recv(), 从内核读数据
1.服务器端通信流程
1.1创建socket 用于监听的套接字
int lfd = socket();
int lfd = socket(AF_INET, SOCK_STREAM, 0);
第一个参数 AF_INET--IPV4 AF_INET--IPV6 AF_LOCAL AF_UNIX--本地进程间通信
第二个参数 传输协议 sock_stream(流式协议) TCP sock_dgran(报式协议)UDP
1.2 将得到的文件描述符和本地的IP端口进行绑定 绑定IP 服务器绑定固定ip和端口
bind();
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
参数一: 用于监听的描述符
参数二:存储本地 IP和端口 (必须是网络字节序的大端,需要进行转换)参数三:计算二的大小
1.3 绑定成功之后开始监听 ,监听有没有客户端建立连接的请求
- 不负责和客户端通信, 负责检测客户端的连接请求, 检测到之后调用accept就可以建立新的连接
listen();
ret = listen(lfd, 128);
参数一: 通信的描述符
参数二:一次性可以监听的文件描述符个数
1.4 等待并接受客户端的链接请求 建立新的连接 会得到一个新的文件描述符
- 没有连接时阻塞 有连接后 建立连接返回通信的描述符
- 负责和建立连接的客户端通信
- 如果有N个客户端和服务器建立了新的连接, 通信的文件描述符就有N个,每个客户端和服务器都对应一个通信的文件描述符
int cfd = accept();
int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
参数一: 套接字通信描述符
参数二:写入客户端的IP 端口
参数三:第二参数的内存大小
1.5通信
// 接收数据
read(); / recv();
// 发送数据
write(); / send();
// 接收数据
ssize_t read(int sockfd, void *buf, size_t size);
ssize_t recv(int sockfd, void *buf, size_t size, int flags);
- 参数:
- sockfd: 用于通信的文件描述符, accept() 函数的返回值
- buf: 指向一块有效内存, 用于存储接收是数据
- size: 参数buf指向的内存的容量
- flags: 特殊的属性, 一般不使用, 指定为 0
// 发送数据的函数
ssize_t write(int fd, const void *buf, size_t len);
ssize_t send(int fd, const void *buf, size_t len, int flags);
- 参数:
- fd: 通信的文件描述符, accept() 函数的返回值
- buf: 传入参数, 要发送的字符串
- len: 要发送的字符串的长度
- flags: 特殊的属性, 一般不使用, 指定为 0
1.6断开连接 关闭套接字 (四次挥手)
close();
通信的文件描述符:
- 客户端和服务器端都有通信的文件描述符
- 发送数据:调用函数 write() / send(),数据进入到内核中
- 接收数据: 调用的函数 read() / recv(), 从内核读数据
close(cfd);
close(lfd);
2.客户端通信流程
2.1.创建套接字
2.2.连接服务器 需要知道服务器绑定的IP和端口
connect();
int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));
2.3.通信
2.4.断开连接
close(fd);
3.服务器端代码
// 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
//自动绑定本地IP地址
// 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));
//accept返回值,接收的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;
}
4.客户端通信代码
// 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)
{
// 发送数据
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;
}
持续更新...
多线程并发服务器创建
作者: 苏丙榅