网络结构模式
C/S结构 - 客户机/服务器;采用两层结构,服务器负责数据的管理,客户机负责完成与用户的交互;C/S结构中,服务器 - 后台服务,客户机 - 前台功能;
优点
1. 充分发挥客户端PC处理能力,先在客户端处理再提交服务器,响应速度快;
2. 操作界面好看,满足个性化需求;
3. 安全性较高,面向固定用户群,程序更注重流程;
缺点
1. 需要安装专用的客户端软件;
2. 对客户端的操作系统有限制,不能跨平台;
B/S结构 - 浏览器/服务器;将系统功能实现的核心部分集中于服务器,简化系统开发,维护;
优点
总体成体低,维护方便,分布性强,开发简单;
缺点
1. 通信开销大,系统和数据的安全性较低;
2. 无法实现个性化的功能要求;
3. 协议固定;
4. 响应速度明显降低;
MAC地址、IP地址、端口
MAC地址
网卡是一块被设计用来允许计算机在计算机网络上进行通讯的计算机硬件,又称为网络透配器或网络接口卡NIC,其拥有 MAC 地址,属于 OS 模型的第 2层。
每个网卡都有一个被称为MAC地址的第一无二的48位串行号(以太网卡/无线网卡);
网卡的功能:
1. 数据封装与解封装
2. 链路管理
3. 数据编译与译码
MAC地址 - 媒体存取控制地址/局域网地址/以太网地址/物理地址/硬件地址
MAC地址是用来确认网络设备位置的地址,由网络设备制造商生产时烧录在网卡中;一台设备可以有多个网卡;
IP地址
IP地址是互联网的协议地址,是IP协议提供的一种统一的地址格式,为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此屏蔽物理地址的差异;
A类IP地址 - 一个字节网络地址,三个字节主机地址;1.0.0.1 - 126.255.255.254;子网掩码255.0.0.0;用于广域网
B类IP地址 - 两个字节网络地址,两个字节主机地址;128.0.0.1 - 191.255.255.254;子网掩码255.255.0.0;用于城际网络
C类IP地址 - 三个字节网络地址,一个字节主机地址;192.0.0.1 - 223.255.255.254;子网掩码255.255.255.0;用于局域网
0.0.0.0 - 当前主机
255.255.255.255 - 当前子网的广播地址
IP地址不能以127开头,127.0.0.1可以代表本机IP地址
子网掩码
子网掩码必须结合IP地址一起使用,用于屏蔽IP地址一部分,区分网络地址和主机地址;
192.168.100.10/24 - IP地址192.168.100.10 , 子网掩码24个1;
端口
设备与外界通讯交流的出口 - 虚拟端口/物理端口;
虚拟端口是逻辑意义上的端口,特指TCP/IP协议中的端口;一个IP地址可以由65536个端口,端口通过端口号标识 0 - 65535;一个计算机中不能出现同样端口号的进程,不用进程号的原因是因为进程号是变化的;0~1023是周知端口,紧密绑定一些特定服务,不能自己设置使用;
1024~49151为注册端口,用户选择安装的一些应用程序
49152~65535 动态分配
网络模型
七层参考模型/osi参考模型
1. 物理层 - 定义物理设备的标准(接口类型、传输速率)
2. 数据链路层 - 提供介质访问、链路管理
3. 网络层 - IP选址、路由选择
4. 传输层 - 建立、管理、维护端到端的连接
5. 会话层 - 建立、管理、维护会话
6. 表示层 - 数据格式转化、数据加密
7. 应用层 - 为应用程序提供服务,用户和网络服务的接口
TCP/IP四层模型
协议
通信双方必须共同遵从的一组约定;三要素:语法、语义、时序,最终体现为在网络上传输的数据包格式;各个层之间的协议不互相影响
应用层协议 - FTP(文件传输)/HTTP(超文本传输协议)/NFS(网络文件协议)
传输层协议 - TCP(传输控制协议)/UDP(用户数据包协议)
网络层协议 - IP(因特网互联协议)/ICMP(因特网控制报文协议)/IGMP(因特网组管理协议)
网络接口层协议 - ARP(地址解析协议)/RARP(反向地址解析协议)
UDP协议
TCP协议
IP协议
以太网帧协议
ARP协议
网络通信的过程
封装 - 上层协议通过封装使用下层协议提供的服务;每层协议在上层数据的基础上加上自己的头部/尾部信息,实现该层的功能;
分用 - 帧到达主机,沿着协议栈自底向上依次传递,各层协议处理本层负责的头部数据,获取信息;
ARP协议 - 通过IP地址查找MAC地址; - 28个字节
RARP协议 - 通过MAC地址查找IP地址;
Socket介绍
套接字 - 对网络中不同主机上的应用进程之间进行双向通信的端点的抽象;一个套接字就是网络上进程通信的一段,提供了应用层进程利用网络协议交换数据的机制;上联应用程序,下联网路协议栈,是应用程序与网络协议进行交互的接口;
通信时其中一个网络应用程序将要传输的一段信息写入它所在主机的 socket 中,该 socket通过与网络接口卡 (NIC)相连的传输介质将这段信息送到另外一台主机的 socket 中,使对方能够接收到这段言息。socket 是由 IP 地址和端口结合的,提供向应用层进程传送数据包的机制。
在linux环境下,用于表示进程间网络通信的特殊文件类型;本质为内核借助缓冲区形成的伪文件;
套接字通信分两部分
服务器端:被动接收连接,一般不主动
客户端:主动向服务器发起连接
socket地址
socket地址是一个结构体 - 封装IP和端口号
sa_family是地址族类型的变量,地址族类型通常与协议族相对应;sa_data存放socket地址值
PF_UNIX - 文件路径名 - 108字节
PF_INET - 6个字节 , 16位端口号,32位IP
PFINET6 - 26个字节 , 16位端口号,32位流标识 ,128位IP,32bit范围ID
为了方便使用提出专用的SOCKET地址:
所有专用socket地址实际使用时都需要转换为通用的socket地址;
socket函数
#inc]ude <sys/types .h>
#incIude <sys/socket .h>
#incTude <arpa/inet .h> // 包含了该头文件上面两个可以省略
int socket(int domain, int type, int protoco1);
功能:创建一个套接字
参数:
domain - 协议族
AF_INET - ipv4
AF_INET6 - ipv6
AF_UNIX AF_LOCAL - 本地套接字通信(进程间)
type - 通信过程中实现的协议类型
SOCK_STREAM - 流式协议
SOCK_DGRAM - 报式协议
protocol - 具体的协议
0 - SOCK_STREAM (TCP)
- SOCK_DGRAM (UDP)
返回值:
成功 - 返回文件描述符,操作的就是内核缓冲区
失败 - -1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:绑定fd和本地的IP/端口
参数:
sockfd - socket函数得到的fd
addr - 需要绑定的socket地址
addrlen - 第二个参数结构体的内存大小
int listen(int sockfd, int backlog);// /proc/sys/net/core/somaxconn
功能:监听socket上的连接
参数:
sockfd - 文件描述符
backlog - 未连接和已连接的和的最大值
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:接收客户端连接,默认阻塞,等待客户端连接进来
参数:
sockfd - 文件描述符
addr - 传出参数,记录了连接成功后客户端的地址信息
addrlen - 第二个参数的内存大小
返回值
成功 - 用于通信的文件描述符
失败 - -1
int connect(int sockfd, const struct sockaddr *addr , socklen_t addrlen) ;
功能:客户端连接服务器
参数:
sockfd - 用于通信的文件描述符
addr - 客户端要连接的服务器的地址信息
addtrlen - 第二个参数的内存大小
返回值:
成功 - 0
失败 - -1
ssize_t write(int fd, const void *buf, size_t count);
ssize_t read(int fd, void *buf,size_t count);
字节序
字节序,顾名思义宁节的顺序,就是大于一个宁节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)
字节序分为大端字节序和小端字节序;
大端小端的判断
/*
字节序判断 - 大端/小端
*/
#include <iostream>
using namespace std;
int main(){
union{
short value;
char byte;
}test;
test.value = 0x0102;
if(test.byte == 1){
cout<<"大端存储"<<endl;
}
else if(test.byte == 2){
cout<<"小端存储"<<endl;
}
else{
cout<<"未知"<<endl;
}
return 0;
}
字节序转换函数
格式化的数据在两台不同字节序的主机之间传递会发生问题;所以需要发送端先转大端,接收端再根据自身情况进行转换;socket提供了封装好的转换函数
s - 转换端口;l - 转换IP
网络通信时,需要将主机字节序转换成网络字节序(大端),另外一段获取到数据以后根据情况将网络字节序转换成主机字节
IP地址转换
将字符串IP转为整数/主机网络字节序转换
#include <arpa/inet.h>
// p - 点分十进制字符串 ; n - 网络字节序的整数
int inet_pton(int af, const char *src, void *dst);
af - 地址族
AF - INET IPV4
AF - INET6 IPV6
src - 需要转换的点分十进制的IP字符串
dst - 转换后的结果保存在这儿
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
size - 第三个参数的大小(数组大小)
返回值 - 转换后的数据地址,和dst一样
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
using namespace std;
// int inet_pton(int af, const char *src, void *dst);
// const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
int main(){
//字符串转整数
char buf[] = "192.168.1.4";
unsigned int num = 0;
inet_pton(AF_INET , buf , &num);
unsigned char* p = (unsigned char*)#
cout<<(int)*p<<" "<<(int)*(p+1)<<" "<<(int)*(p+2)<<" "<<(int)*(p+3)<<endl;
// 整数转字符串
char ip[16] = "";
const char* str = inet_ntop(AF_INET , &num , ip , sizeof(ip));
cout<<str<<endl;
return 0;
}
TCP通信流程
UDP | TCP |
用户数据包协议 | 传输控制协议 |
面向无连接 | 面向连接 |
可以单播,多播,广播 | 只能1v1(单播) |
面向数据报 | 基于字节流 |
不可靠的协议 | 可靠的协议 |
TCP 通信流程
服务器
- 创建有一个用于监听的套接字
- 将监听的文件描述符和本地的IP、端口绑定(IP 端口就是服务器的地址信息)
- 设置监听,监听的fd开始工作
- 阻塞等待,当有客户端发起连接,解除阻塞,接受客户端连接,得到和客户端通信的套接字
- 通信(接受数据/发送数据)
- 通信结束断开连接
客户端
- 创建一个用于通信的套接字
- 连接服务器,需要指定连接服务器的IP/端口
- 连接成功了,发生通信
- 通信结束断开连接
TCP通信实现(服务端/客户端)
// 实现TCP服务器端
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <unistd.h>
#include <string.h>
using namespace std;
int main(){
// 1. 创建socket(用于监听)
int lfd = socket(AF_INET , SOCK_STREAM , 0);
// 2. 绑定
struct sockaddr_in saddr;
saddr.sin_family = PF_INET;
inet_pton(AF_INET , "192.168.93.129" , &saddr.sin_addr.s_addr);
saddr.sin_port = htons(9998);
bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));
// 3. 监听
listen(lfd , 8);
// 4. 接收客户端连接
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);
// 输出客户端的信息
char client_ip[16];
inet_ntop(AF_INET , &caddr.sin_addr.s_addr , client_ip , sizeof(client_ip));
unsigned short client_port = ntohs(caddr.sin_port);
cout<<"IP:"<<client_ip<<" "<<"PORT: "<<client_port<<endl;
// 获取客户端的数据
char buf[1024] = {0};
len = read(cfd , buf , sizeof(buf));
cout<<"服务器读取数据:"<<buf<<endl;
// 给客户端发数据
const char *str = "hello 647";
write(cfd , str , strlen(str));
close(lfd);
close(cfd);
return 0;
}
// TCP通信客户端
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <unistd.h>
#include <string.h>
using namespace std;
int main(){
// 1. 创建套接字
int fd = socket(AF_INET , SOCK_STREAM , 0);
// 2. 连接服务器端
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
inet_pton(AF_INET , "192.168.93.129" , &caddr.sin_addr.s_addr);
caddr.sin_port = htons(9998);
connect(fd , (struct sockaddr*)&caddr , sizeof(caddr));
// 3. 读写数据
const char *str = "hello zry";
write(fd , str , strlen(str));
char buf[1024] = {0};
int len = read(fd , buf , sizeof(buf));
cout<<"客户端读取数据:"<<buf<<endl;
close(fd);
return 0;
}
TCP三次握手
三次握手发生在客户端丽连接,调用connect(),底层会通过TCP协议进行三次握手;
注意:第三次握手可以携带数据
滑动窗口
滑动窗口的大小意味着接收方还有多大的缓冲区可用于接收数据;
滑动窗口的大小会随着发送数据/接收数据而变化;
通信双方都有发送缓冲区和接收缓冲区
mss: 一条数据最大的数据量;
win: 滑动窗口;
TCP四次挥手
发生在断开连接的时候,程序调用close()会使用TCP协议进行四次挥手;
客户端/服务端都可以主动发起/断开连接,谁调用close()就是谁发起的;
注意:发起方FIN请求可以携带数据!!
多进程实现并发服务器
实现TCP通信服务器处理并发的任务,使用多线程或者多进程解决;
1. 一个父进程,多个子进程
2. 父进程负责等待并接收客户端的连接文章来源:https://www.toymoban.com/news/detail-702803.html
3. 子进程:完成通信,接收一个客户端请求就创建一个子进程文章来源地址https://www.toymoban.com/news/detail-702803.html
#include <iostream>
#include <arpa/inet.h>
#include<cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
using namespace std;
void fun(int arg){
while(1){
int ret = waitpid(-1 , NULL , WNOHANG);
if(ret == - 1){
break;
}
else if(ret == 0){
break;
}
else{
cout<<"回收到了子进程: "<<ret<<endl;
}
}
}
int main(){
// 注册信号捕捉
struct sigaction act;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
act.sa_handler = fun;
sigaction(SIGCHLD , &act , NULL);
// 创建
int lfd = socket(AF_INET , SOCK_STREAM , 0);
if(lfd == -1){
perror("socket");
exit(0);
}
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));
if(ret == -1){
perror("bind");
exit(0);
}
// 监听
ret = listen(lfd , 5);
if(ret == -1){
perror("listen");
exit(0);
}
// 不断循环等待客户端连接
while(1){
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);
if(cfd == -1){
if(errno == EINTR){
continue;
}
perror("accept");
exit(0);
}
// 每一个连接进来,创建子进程与客户端通信
pid_t pid = fork();
if(pid == 0){
// 获取客户端信息
char ip[16];
inet_ntop(AF_INET , &caddr.sin_addr.s_addr , ip , sizeof(ip));
unsigned short port = ntohs(caddr.sin_port);
cout<<"IP: "<<ip<<" "<<"port: "<<port<<endl;
// 接收客户端发来的数据
char buf[1024] = {0};
while(1){
int len = read(cfd , &buf , sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}
else if(len > 0){
cout<<"读到了数据:"<<buf<<endl;
}
else{
cout<<"已经断开连接了.....";
}
write(cfd , buf , strlen(buf));
}
}
close(cfd);
}
close(lfd);
return 0;
}
// TCP通信的客户端
#include <iostream>
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
using namespace std;
int main() {
// 1.创建套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1) {
perror("socket");
exit(-1);
}
// 2.连接服务器端
struct sockaddr_in caddr;
caddr.sin_family = AF_INET;
inet_pton(AF_INET , "192.168.93.129" , &caddr.sin_addr.s_addr);
caddr.sin_port = htons(9999);
int ret = connect(fd, (struct sockaddr *)&caddr, sizeof(caddr));
if(ret == -1) {
perror("connect");
exit(-1);
}
// 3. 通信
char recvBuf[1024];
int i = 0;
while(1) {
sprintf(recvBuf, "data : %d\n", i++);
// 给服务器端发送数据
write(fd, recvBuf, strlen(recvBuf)+1);
int len = read(fd, recvBuf, sizeof(recvBuf));
if(len == -1) {
perror("read");
exit(-1);
} else if(len > 0) {
printf("recv server : %s\n", recvBuf);
} else if(len == 0) {
// 表示服务器端断开连接
printf("server closed...");
break;
}
sleep(1);
}
// 关闭连接
close(fd);
return 0;
}
多线程实现并发服务器 - client同多进程并发
#include <iostream>
#include <arpa/inet.h>
#include <cstdio>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
using namespace std;
struct sockInfo{
int fd;
pthread_t tid;
struct sockaddr_in addr;
};
struct sockInfo sockinfos[128];
void *work(void * arg){
// 子线程和客户端通信 cfd/客户端信息/线程号
// 获取客户端信息
struct sockInfo *pinfo = (struct sockInfo *)arg;
char ip[16];
inet_ntop(AF_INET , &pinfo->addr.sin_addr.s_addr , ip , sizeof(ip));
unsigned short port = ntohs(pinfo->addr.sin_port);
cout<<"IP: "<<ip<<" "<<"port: "<<port<<endl;
// 接收客户端发来的数据
char buf[1024] = {0};
while(1){
int len = read(pinfo->fd , &buf , sizeof(buf));
if(len == -1){
perror("read");
exit(0);
}
else if(len > 0){
cout<<"读到了数据:"<<buf<<endl;
}
else{
cout<<"已经断开连接了.....";
break;
}
write(pinfo->fd , buf , strlen(buf)+1);
}
close(pinfo->fd);
return NULL;
}
int main(){
// 创建
int lfd = socket(AF_INET , SOCK_STREAM , 0);
if(lfd == -1){
perror("socket");
exit(0);
}
cout<<1;
// 绑定
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(9999);
saddr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd , (struct sockaddr*)&saddr , sizeof(saddr));
if(ret == -1){
perror("bind");
exit(0);
}
// 监听
ret = listen(lfd , 5);
if(ret == -1){
perror("listen");
exit(0);
}
// 初始化数据
int max = sizeof(sockinfos)/sizeof(sockinfos[0]);
for(int i = 0 ; i<max ; i++){
bzero(&sockinfos[i] , sizeof(sockinfos[i]));
sockinfos[i].fd = -1;
sockinfos[i].tid = -1;
}
// 不断循环等待客户端连接(子线程创建)
while(1){
struct sockaddr_in caddr;
socklen_t len = sizeof(caddr);
int cfd = accept(lfd , (struct sockaddr*)&caddr , &len);
struct sockInfo *pinfo;
for(int i = 0 ; i<max ; i++){
// 从数组中找到可用的sockInfo
if(sockinfos[i].fd == -1){
pinfo = &sockinfos[i];
break;
}
if(i == max - 1){
sleep(1);
i--;
}
}
pinfo->fd = cfd;
memcpy(&pinfo->addr , &caddr , len);
// 每一个连接进来,创建子线程与客户端通信
pthread_t tid;
pthread_create(&pinfo->tid , NULL , work , pinfo);
pthread_detach(pinfo->tid);
}
close(lfd);
return 0;
}
到了这里,关于linux并发服务器 —— linux网络编程(七)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!