一. 网络编程
1. 网络基础
1.1 网络的作用?
网络是用于解决远距离,跨主机的数据通讯。
1.2 网络体系结构
将数据从一台主机借助网络传输到另一台主机其实是一个复杂的过程,
系统如何保证数据的完整,一致并安全送到目标主机,网络系统将整个
数据传输过程划为为不同的阶段。每一阶段完成自身的功能,并为下一阶段
提供相关服务,每一阶段形成的相关协议总和。形成了网络体系结构。
两种重要的体系结构模型:
1. OSI 七层协议模型
物理层 数据链路层 网络层 传输层 会话层 表示层 应用层
2. TCP/IP 四层协议模型 (工业标准)
网络接口层 网络层 传输层 应用层
1.3 传输层的两个协议
1) TCP (传输控制协议) Transmit control protocol、
向应用层提供可靠的面向连接的数据传输服务
面向连接: 通讯双方必须先建立网络连接。然后才能收发数据
网络连接的建立过程:(三次握手)
第一次握手: 客户端发送SYN报文给服务器,发起连接请求
第二次握手: 服务器发送SYN+ACK报文给客户端,确认连接请求
第三次握手: 客户端发送ACK报文给服务器,此时连接正式建立
网络的断开过程:(四次挥手)
第一次挥手: 客户端发送FIN报文给服务器,请求断开客户端到服务器方向的数据传输
第二次挥手: 服务器发送ACK报文给客户端,确认客户端到服务器方向的数据传输的断开
第三次挥手: 服务器发送FIN报文给客户端,请求断开服务器到客户端方向的数据传输
第四次挥手: 客户端发送ACK报文给服务器,此次完全断开
2) UDP (用户数据报协议) User Datagram protocol、
向应用层提供不可靠的面向无连接的数据传输服务
UDP协议的特点:
1. 无连接不可靠
2. 无连接不需要系统维护连接状态,节省系统资源;
3. UDP可实现一对多通讯,TCP只能实现一对一
4. UDP实时性高,如果对数据的可靠性需求不高,但对数据传输的实时性有严格要求
优先选择使用UDP协议。
1.4 网络相关术语
1) IP 地址
网络中主机的标识
本质(IPV4): 是一个32位的无符号整型数
IP的表现形式:
1) 32位的无符号整型数
2) 点分十进制的字符串 192.168.14.34
IP的组成:
1) 网络ID (表明某主机处于哪一个网络)
2) 主机ID (标识处于同一个网络的不同主机)
IP的分类
点分十进制第一个字节范围
A 1~126
B 128~191
C 192~223
D 224~239 UDP组播通讯
E 240~254
2) 端口号
用于标识主机中应该由哪一个往往进程来处理网络数据。
本质: 是一个16位的无符号整型数
可供用户使用的端口:
1024 ~ 65535
尽量避免和已有的网络程序使用相同端口号
3) 网络字节序
作用:避免数据因不同主机的数据存储方式不同而造成对数据解析的不一致情况的发生。
网络字节序要求不同主机对网络数据的存储均采用大端字节序的方式
4) socket (套接字)
套接字是网络编程的接口,通过socket 来使用传输层提供的数据传输服务的。
简单理解: 可以将socket 理解为网络设备的设备描述符,操作socket 相当于
操作网络设备,从而达到利用网络设备传输网络数据的目的。
常用类型:
SOCK_STREAM 流式套接字 基于TCP协议
SOCK_DGRAM 数据报套接字 基于UDP协议
...
2. 网络编程
2.1 基于UDP协议的网络编程
服务器端:
1. 创建套接字(SOCK_DGRAM)
socket
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int socket(int domain, int type, int protocol);
函数功能: 创建套接字
函数参数: domain: 协议族,主要取值为
AF_INET IPV4
AF_INET6 IPV6
type: 套接字类型 (常用)
SOCK_STREAM 流式套接字
SOCK_DGRAM 数据报套接字
SOCK_RAW 原始套接字
protocol: 通讯协议,一般写0,让系统决定使用的协议
函数返回值: 成功返回 套接字描述符
失败返回-1,错误码存入errno
2. 绑定自身地址信息
bind
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int bind(int sockfd, struct sockaddr* addr, int addrlen);
函数功能: 绑定地址信息到套接字
函数参数: sockfd: 套接字描述符
addr: 存放地址信息的结构体变量地址
addrlen: 地址信息结构体的长度
函数返回值: 成功返回 0
失败返回-1,错误码存入errno
3. 接收客户端网络数据
recvfrom
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int recvfrom(int sockfd, void *buf, int len, unsigned int flags,
struct sockaddr *from, int *fromlen);
函数功能: 接收网络数据
函数参数: sockfd: 套接字描述符
buf: 存放接收到的网络数据的内存缓冲区地址
len: 待接收的数据长度
flags: 接收标志 一般写0
from: 用于接收对方的地址结构体指针
fromlen:用于获取地址结构体长度的指针
函数返回值: 成功返回 实际接收到的数据字节数
失败返回-1,错误码存入errno
4. 发送网络数据给客户端
sendto
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int sento(int sockfd, const void *buf, int len, unsigned int flags,
struct sockaddr *to, int tolen);
函数功能: 接收网络数据
函数参数: sockfd: 套接字描述符
buf: 存放待发送的网络数据的内存缓冲区地址
len: 待发送的数据长度
flags: 接收标志 一般写0
to: 目标主机的地址结构体指针
tolen: 地址结构体长度
函数返回值: 成功返回 实际发送的数据字节数
失败返回-1,错误码存入errno
5. 回收套接字
延伸: 网络字节序转换:
htonl long h:host n: network
htons short
ntohl
ntohs
htonl
htons
2.2 基于UDP协议的广播通讯
原则: 广播通讯是要借助广播地址的
广播地址:
1. 直接广播地址
主机ID部分所有的二进制位数据都有1:
例如某台主机IP为 192.168.1.5 则 192.168.1.255 就可作为 192.168.1 所标识的网络的
直接广播地址;
2. 本地广播地址:
255.255.255.255
INADDR_BROADCAST
广播通讯的设计流程:
1.发送端
1) 创建套接字
2) 为套接字设置广播属性
setsockopt
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int setsockopt(int sockfd, int level, int sockopt,
const void* optval,socklen_t optlen);
函数功能: 设置套接字属性选项
函数参数: sockfd: 套接字描述符
level: 指定控制套接字的层次.可以取三种值:
1)SOL_SOCKET:通用套接字选项.
2)IPPROTO_IP:IP选项.
3)IPPROTO_TCP:TCP选项.
sockopt: 待设置的套接字属性选项名称
optval: 套接字属性选项的取值
optlen: 套接字属性选项值的长度
函数返回值: 成功返回 0
失败返回-1,错误码存入errno
例子:
int enable = 1;
setsockopt(sockfd,SOL_SOCKET,SO_BROADCAST,&enable,sizeof(int));
3) 向广播地址进行数据发送
4) 接收回复的数据
5) 关闭套接字
2.接收端
1) 创建套接字
2) 必须绑定任意IP(INADDR_ANY)和广播端口到套接字上
3) 接收发送端广播的数据
4) 回复数据给发送端
5) 关闭套接字
2.3 基于UDP协议的组播通讯
原则: 组播通讯是要借助组播地址的
组播地址: D 类 IP
组播通讯的设计流程:
发送端:
1) 创建套接字
2) 指定组播地址和组播端口
3) 发送信息到组播地址和组播端口,
4) 接收回复数据
5) 关闭套接字
接收端:
1) 创建套接字
2) 加入组播组
#include <net/if.h>
struct ip_mreqn
{
struct in_addr imr_multiaddr; /* IP multicast group address */
struct in_addr imr_address; /* IP address of local interface */
int imr_ifindex; /* interface index */
};
struct ip_mreqn mreqn = {0};
mreqn.imr_multiaddr.s_addr = inet_addr(argv[1]) ;
mreqn.imr_address.s_addr = inet_addr("192.168.14.32") ;
mreqn.imr_ifindex = if_nametoindex("ens33");
setsockopt(sockfd,IPPROTO_IP,IP_ADD_MEMBERSHIP,&mreqn,sizeof(struct ip_mreqn));
3) 必须绑定任意IP(INADDR_ANY)和组播端口到套接字上
4) 接收组播数据
5) 发送回复数据给发送端
6) 离开组播组
setsockopt(sockfd,IPPROTO_IP,IP_DROP_MEMBERSHIP,&mreqn,sizeof(struct ip_mreqn));
7) 关闭套接字
3.1 基于TCP协议的网络编程
服务器端
在基于TCP协议的服务器端开发过程中,要涉及两种作用的套接字
一种是专用于连接监听的套接字 (监听套接字)
一种是专用于和客户端数据通讯的套接字 (通讯套接字)
1.创建流式socket;(socket)
2.绑定自己的地址信息;(bind)
3.监听客户端连接(listen)
listen
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int listen(int sockfd, int backlog);
函数功能: 绑定地址信息到套接字
函数参数: sockfd: 监听套接字描述符
backlog:可同时排队连接的最大链接数
函数返回值: 成功返回 0
失败返回-1,错误码存入errno
4.接收客户端的连接(accept)
accept
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int accept(int sockfd, struct sockaddr* addr, int addrlen);
函数功能: 接受客户端连接,并生成通讯套接字
函数参数: sockfd: 监听套接字描述符
addr: 用于存放连接方地址信息的结构体变量地址
addrlen:地址信息结构体的长度
函数返回值: 成功返回 用于通讯的套接字
失败返回-1,错误码存入errno
5.接收客户端的信息(recv)
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int recv(int sockfd, void *buf, int len, unsigned int flags);
函数功能: 接收网络数据
函数参数: sockfd: 套接字描述符
buf: 存放接收到的网络数据的内存缓冲区地址
len: 待接收的数据长度
flags: 接收标志 一般写0
函数返回值: 成功返回 实际接收到的数据字节数
失败返回-1,错误码存入errno
6.发送消息给客户端(send)
send
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int send(int sockfd, const void *buf, int len, unsigned int flags);
函数功能: 接收网络数据
函数参数: sockfd: 套接字描述符
buf: 存放待发送的网络数据的内存缓冲区地址
len: 待发送的数据长度
flags: 接收标志 一般写0
函数返回值: 成功返回 实际发送的数据字节数
失败返回-1,错误码存入errno
7.关闭套接字 (close)
客户端
1.创建socket; (socket)
2.连接服务器(connect)
connect
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int connect(int sockfd, struct sockaddr* addr, int addrlen);
函数功能: 向服务器端发送连接
函数参数: sockfd: 监听套接字描述符
addr: 用于存放服务器地址信息的结构体变量地址
addrlen:地址信息结构体的长度
函数返回值: 成功返回 0
失败返回-1,错误码存入errno
3.发送消息到服务器(send)
4.接收服务端的信息(recv)
5.关闭套接字 (close)
基于TCP协议的并发服务器设计
1. Socket + thread
思想: 一旦客户端连接,服务器端就产生一个线程,在线程中专门与客户端进行数据通讯,
主线程负责客户端连接
优点: 实现简单,思想清晰
缺点: 效率低下,服务器将频繁的创建和销毁线程。
2. 线程池
思想:在实际的业务请求尚未达到时,先创建一定数量的线程,且让线程处于休眠状态,此时程序或者进程
就犹如一个容器,容纳了多个线程,我们形象的称为线程池。
当业务请求达到时,投递一个任务到任务队列,并唤醒处于休眠状态的线程,线程再竞争执行任务队列
中的任务,当线程处理完某个任务后,线程不退出,而是接着竞争处理下一个任务。
设计实现:
1. 线程池初始化:
typedef struct
{
void*(*task)(void*);
void* argp;
}task_t;
typedef struct node
{
task_t job;
struct node *next;
}node_t;
typedef struct
{
int thread_num; //线程数量
pthread_t *ptids; //用于存储线程ID的内存首地址
int queue_max_num; //任务队列的队列容量
int queue_cur_num; //任务对列中当前任务数量
node_t *head; //队列头指针
node_t *tail; //队列尾指针
pthread_mutex_t mutex; //用于访问队列的互斥锁
pthread_cond_t queue_empty; //用于阻塞线程的条件变量
pthread_cond_t queue_full; //用于任务条件模块的条件变量
}threadpool;
threadpool* threadpool_init(int,int);
2. 线程函数:
功能: 访问任务队列,取出队列中的每一个任务元素,执行元素任务函数;
3. 任务添加函数:
功能: 生成任务元素,并将任务元素尾插到任务队列中,供线程执行;
优点: 相对于socket+threads 实现方式来说,效率高,使用简单
缺点: 线程池设计实现难度较大。
3. 多路复用技术 (select poll epoll)
目的: 用于解决阻塞问题的;
原理: 监测描述符的状态,当状态满足时,多路复用技术会告诉进程/线程, 相关操作
可以在非阻塞的方式下,此次 IO 操作是可以避免阻塞的;
select技术: 轮询监测多个描述符的可读,可写,异常状态,如果描述符状态变化了
会进行标记,开发人员可以根据标记来进行IO操作,此时的操作是非阻塞的;
select技术的实现:
select 函数:
头文件: #include <sys/socket.h>
#include <sys/types.h>
函数原型: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds
struct timeval* timeout);
函数功能: 监测描述符的IO状态
函数参数: nfds: 待监测描述符最大值+1
readfds: 存放监测可读状态的描述符集合
writefds: 存放监测可写状态的描述符集合
exceptfds: 存放监测异常状态的描述符集合
timeout: 超时时间,如果设为NULL,select将阻塞等待直到有描述符状态变化
如果timeout成员均为0,则select变为非阻塞方式,否则select 等待设置的时间后返回。
函数返回值: 成功返回 发生状态变化的描述符个数
0: 表明超时
失败返回-1,错误码存入errno