一、网络协议--UDP
UDP位于传输层,全称为 transform control Protocol(传输控制协议/流式套接字)
1.1 TCP与UDP的比较
UDP的特点:
-
面向数据包
-
无连接
-
尽最大努力交付,不安全不可靠(数据丢包、数据乱序)
-
机制简单,资源开销小,数据实时性高
-
可实现一对一、一对多的通信
TCP的特点:
-
面向数据流
-
有连接(通信之前必须建立连接)
-
安全可靠的传输机制
-
机制复杂,网络资源开销大
-
本质只能实现一对一的通信(使用TCP并发方式可实现一对多通信)
1.2 TCP三次握手和四次挥手机制
TCP三次握手:TCP建立连接时,需要进行三次握手,为了确保收发双方通信之前都已准备就绪。

ACK:响应报文标志;
SYN:请求建立连接标志位;
三次握手:开始时,发送端向接收端发送SYN请求信号请求接收;接收端发送是哪个ACK应答信号表示接收到请求,同时发送SYN请求信号;发送端发送ACK应答信号,开始传输数据;
TCP四次挥手:TCP断开连接时,需要进行四次挥手,确保断开连接前双方都已通信结束。

FIN:断开连接的标志;
四次挥手:结束阶段,发送端发送FIN结束信号表示结束发送数据,接收端发送ACK应答表示接收到信号,接收端再次发送FIN和ACK信号,表示请求中止接受和应答信号,最后发送端发送ACK应答信号,传输结束。
1.3 TCP的编程流程

int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
功能:请求与服务端建立连接
参数:
sockfd:套接字
addr:要连接的服务端的地址信息
addrlen:服务端地址大小
返回值:
成功:0
失败:-1
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:发送网络数据
参数:
sockfd:网络套接字
buf:要发送的数据首地址
len:发送的字节数
flags:0 :按照默认方式发送
返回值:
成功:实际发送的字节数
失败:-1
int listen(int sockfd, int backlog);
功能:监听建立三次握手的客户端
参数:
sockfd:监听套接字
backlog:最大允许监听的客户端个数
返回值:
成功:0
失败:-1
int accept(int socket, struct sockaddr *restrict address,
socklen_t *restrict address_len);
功能:接收建立三次握手的客户端,并产生一个通讯套接字
参数:
socket:监听套接字
address:客户端的地址信息
address_len:客户端地址长的指针
返回值:
成功:通讯套接字
失败:-1
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:从网络套接字上接收数据
参数:
sockfd:通讯套接字
buf:存放接收数据的首地址
len:期望接收到的字节数
flag : 0:默认方式接收(阻塞)
返回值:
成功:实际接收到的字节数
失败:-1
对方断开连接:0
实例:
cs
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/wait.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd < 0)
{
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.190");
int ret = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret < 0)
{
perror("bind error");
return -1;
}
ret = listen(sockfd,10);
if(ret < 0)
{
perror("listen error");
return -1;
}
int connfd = accept (sockfd,NULL,NULL);
if(connfd < 0)
{
perror("accept error");
return -1;
}
char buff[1024] = {0};
ssize_t cnt = recv(connfd,buff,sizeof(buff),0);
if(cnt < 0)
{
printf("recv error");
return -1;
}
printf("cnt = %ld,buff = %s\n", cnt, buff);
cnt = recv(connfd,buff,sizeof(buff),0);
if(cnt < 0)
{
printf("recv error");
return -1;
}
printf("cnt = %ld,buff = %s\n", cnt, buff);
close(connfd);
close(sockfd);
return 0;
}
cs
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/wait.h>
int main(void)
{
int sockfd = socket(AF_INET, SOCK_STREAM,0);
if(sockfd < 0)
{
perror("sockst error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.190");
int ret = connect(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret < 0)
{
perror("connect error");
return -1;
}
ssize_t cnt = send(sockfd,"hello world",11,0);
if(cnt < 0)
{
perror("send error");
return -1;
}
printf("cnt = %ld\n",cnt);
cnt = send(sockfd,"how are you",11,0);
if(cnt < 0)
{
perror("send error");
return -1;
}
printf("cnt = %ld\n",cnt);
close(sockfd);
return 0;
}
1.4 TCP粘包问题
TCP粘包问题:发送方应用层发送的多包数据,将来在接收方可能一次读到,多包数据产生了粘连。
原因:
-
发送方速度较快,TCP底层可能对多包数据进行重新组帧;
-
接收方数据处理速度较慢,导致多包数据在接收缓冲区缓存,应用层读时,一次将多包数据读出。
解决粘包问题的常用方法:
-
调整发送速率
-
发送指定大小,将来接收方也接受指定大小。
结构体
注意:
- 跨平台之间的数据传输时,注意结构体对齐问题。
struct a
{
char a;
int b;
long c;
};
32bits平台《--》64位平台
- 应用层位发送的数据增加分隔符,利用分隔符解析
hello world\nhow are you\n
- 封装自定义数据帧格式进行发送(协议),严格根据协议进行解析。
AA C0 00 00 00 F0 00 BB 10 A0 00 00 00 10 校验 BB AA C0 00 00 00 F0 00 BB 10 A0 00 00 00 10 校验 BB AA C0 00 00 00 F0 00 BB 10 A0 00 00 00 10 校验 BB
帧头:AA
帧尾:BB
有效数据长度:C0
有效数据:00 00 00 F0 00 BB 10 A0 00 00 00 10
校验:
8位和校验
16位和校验
CRC校验
1.5 TCP的其他机制
TCP头部的标志位:
SYN:请求建立连接标志位
ACK:响应报文标志位
PSH:携带数据标志位,通知接收方该从缓冲区读数据
FIN: 请求断开连接标志位
RST:复位标志位
URG: 紧急数据标志位

机制:
1.三次握手,四次挥手;
2.应答机制:TCP对于每一包数据都会给出相应的应答。发送数据时序列号表示这包数据的起始编号,响应报文中的确认号是接受方收到的最后一个字节编号+1;
3.超时重传机制:当数据发送出去等待指定时间没有收到响应,此时认为这包数据丢失,则进行重传;
4.滑动窗口机制:一段缓冲区,缓存TCP已发送未收到响应,准备发送等数据,确保重传时找到数据

提高效率的机制:
延迟应答机制:发送数据时同时等待应答;
流量控制机制:结合TCP头部的窗口大小,动态调整发送速率
捎带应答机制:ACK报文可能和应用层的数据同时发送;
二、HTTP协议
WWW:万维网
问题:
1.万维网服务器后台如何标记万维网数据:URL;
2.万维网客户端与万维网服务器之间使用什么方式通信(HTTP:超文本传输协议);
超文本:文字、链接、音频、视频集合一体的文本;
3.万维网客户端如何展示请求的数据(HTML:超文本标记语言);
URL :统一资源定位符

https://www.baidu.com 端口号可省略
HTTP:超文本传输协议,位于应用层,基于传输层的TCP协议
端口:80
备用端口:8080
(1)HTTP通信过程
客户端建立与服务端的TCP链接,再发送HTTP请求报文,服务端发送HTTP回应报文,任意一方均可发送断开TCP连接。

长连接:发完回应报文会连续一定时间,不会立刻断开 keep--->alive
短链接:发完立刻断开 close
(2)HTTP的报文格式



GET/ HTTO/1.1\r\n GET 后面的/是主页
请求报文:

响应报文:

三、TCP的并发
单循环服务器:服务端同一时刻只能处理一个客户端的任务
并发服务器:服务端同一时刻可以处理多个客户端的任务
并发模型:
(1)多进程
进程资源开销大;安全性高;
实例:
cs
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/wait.h>
#include <pthread.h>
int init_tcp_ser()
{
int sockfd = socket(AF_INET, SOCK_STREAM,0);
if(sockfd < 0)
{
perror("sockst error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.190");
int ret = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret < 0)
{
perror("bind error");
return -1;
}
ret = listen(sockfd,10);
if(ret < 0)
{
perror("listen error");
return -1;
}
return sockfd;
}
int main(void)
{
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int sockfd = init_tcp_ser();
if(sockfd < 0)
{
return -1;
}
while(1)
{
int connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
if(connfd < 0)
{
perror("accept error");
return -1;
}
pid_t pid = fork();
if(pid > 0)
{
}
else if(0 == pid)
{
char buff[1024] = {0};
while(1)
{
memset(buff,0,sizeof(buff));
size_t cnt = recv(connfd,buff,sizeof(buff),0);
if(cnt < 0)
{
perror("recv error");
break;
}
else if(0 == cnt)
{
printf("[%s : %d] : offline\n", inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
break;
}
printf("[%s : %d] : %s\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port),buff);
strcat(buff,"----ok");
cnt = send(connfd,buff,strlen(buff),0);
if(cnt < 0)
{
perror("send error");
break;
}
}
close(connfd);
}
}
close(sockfd);
return 0;
}
(2)多线程
资源开销小;相同资源环境下,并发量比进程大
实例:
cs
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/wait.h>
#include <pthread.h>
#include<stdlib.h>
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
int init_tcp_ser()
{
int sockfd = socket(AF_INET, SOCK_STREAM,0);
if(sockfd < 0)
{
perror("sockst error");
return -1;
}
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));//避免被占用
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.190");
int ret = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret < 0)
{
perror("bind error");
return -1;
}
ret = listen(sockfd,10);
if(ret < 0)
{
perror("listen error");
return -1;
}
return sockfd;
}
void *send_msg(void *arg)
{
int connfd = *((int *)arg);
char buff[1024] = {0};
while(1)
{
memset(buff,0,sizeof(buff));
size_t cnt = recv(connfd,buff,sizeof(buff),0);
if(cnt < 0)
{
perror("recv error");
break;
}
else if(0 == cnt)
{
printf("[%s : %d] : offline\n", inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port));
break;
}
printf("[%s : %d] : %s\n",inet_ntoa(cliaddr.sin_addr),ntohs(cliaddr.sin_port),buff);
strcat(buff,"----ok");
cnt = send(connfd,buff,strlen(buff),0);
if(cnt < 0)
{
perror("send error");
break;
}
}
close(connfd);
}
int main(void)
{
pthread_t tid;
int sockfd = init_tcp_ser();
if(sockfd < 0)
{
return -1;
}
while(1)
{
int connfd = accept(sockfd,(struct sockaddr *)&cliaddr,&clilen);
if(connfd < 0)
{
perror("accept error");
return -1;
}
pthread_create(&tid, NULL, send_msg, &connfd);
pthread_detach(tid);
}
close(sockfd);
return 0;
}
(3)线程池
为了解决多线程或者多进程模型,在服务器运行过程中,频繁创建和销毁线程(进程)bi带来的时间消耗问题;
基于生产者和消费者编程模型,以及任务队列等,实现的一套多线程模型;
(4)IO多路复用
I--->O:fd
对多个文件描述符的读写可以复用一个进程。
在不创建新的进程和线程的前提下,使用一个进程实现对多个文件读写的同时检测;
fgets(stdin);
recv(connfd);
阻塞IO模式:
1.多个任务之间是同步的效果
阻塞IO实例:
cs
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h> /* See NOTES */
#include<sys/socket.h>
#include<netinet/in.h>
#include<netinet/ip.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/wait.h>
#include <pthread.h>
#include<stdlib.h>
#include<sys/time.h>
#include<sys/select.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, const char *argv[])
{
char buff[1024] = {0};
int sockfd = socket(AF_INET, SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket error");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_port = htons(50000);
seraddr.sin_addr.s_addr = inet_addr("192.168.0.190");
int ret = bind(sockfd,(struct sockaddr *)&seraddr,sizeof(seraddr));
if(ret < 0)
{
perror("bind error");
return -1;
}
ret = listen(sockfd,100);
if(ret < 0)
{
perror("listen error");
return -1;
}
fd_set rdfds;
fd_set rdfdstmp;
FD_ZERO(&rdfds);
FD_SET(sockfd,&rdfds);
int maxfd = sockfd;
struct sockaddr_in cliaddr;
socklen_t clilen = sizeof(cliaddr);
while(1)
{
rdfdstmp = rdfds;
int cnt = select(maxfd + 1, &rdfdstmp, NULL, NULL, NULL);
if (cnt < 0)
{
perror("select error");
return -1;
}
if(FD_ISSET(sockfd,&rdfdstmp))
{
int connfd = accept(sockfd, (struct sockaddr *)&cliaddr,&clilen);
if(connfd < 0)
{
perror("accept error");
return -1;
}
FD_SET(connfd,&rdfds);
maxfd = maxfd > connfd ? maxfd : connfd;
}
for(int i = sockfd + 1;i <= maxfd; i++)
{
if(FD_ISSET(i,&rdfdstmp))
{
memset(buff,0,sizeof(buff));
ssize_t cnt = recv(i,buff,sizeof(buff),0);
if(cnt < 0)
{
perror("recv error");
FD_CLR(i,&rdfds);
close(i);
continue;
}
else if(0 == cnt)
{
FD_CLR(i,&rdfds);
close(i);
continue;
}
printf("%s\n",buff);
strcat(buff,"----ok");
cnt = send(i,buff,strlen(buff),0);
if(cnt < 0)
{
perror("send error");
FD_CLR(i,&rdfds);
close(i);
continue;
}
}
}
}
close(sockfd);
return 0;
}
1)select实现IO多路复用:
1.创建文件描述符集合; fd_set
2.添加关注的文件描述符到集合; FD_SET()
3.使用select传递集合表给内核,内核开始检测事件; select()
4.当内核检测到事件时,应用层select将解除阻塞,并获得相关的事件结果;
5.根据select返回的结果做不同的处理;
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
功能:传递文件描述符给内核并等待获取事件结果;
参数:
nfds:关注的最大文件描述符+1;
readfds:读事件关注的文件描述符集合;
writefds:写事件的文件描述符集合;
timeout:设置select检测时的超时时间;
NULL:不设置超时时间(select一直阻塞等待)
返回值:
成功:返回内核监测的到达事件的个数;
失败:-1
0:超时时间到达但没有事件发生,返回0;
2)epoll实现IO多路复用:
1.创建一个文件描述符集合;
2.添加关注的文件描述符;
3.epoll通知内核开始进行事件监测;
4.epoll返回时,获取到到达事件的结果;
5.根据到达事件做任务处理
epoll_create(int size)
功能:通知内核创建文件描述符集合;
参数:
size:监测的文件描述符个数;
返回值:
成功:文件描述符(内核创建的集合表);
失败:-1;
epoll_ctl
功能:对epoll的文件描述符集合进行操作;
参数:
epfd:创建的epoll集合;
op:对文件描述符集合进行的操作;
EPOLL_CTL_ADD :添加文件描述符到集合;
EPOLL_CTL_MOD :修改集合中的文件描述符;
EPOLL_CTL_DEL :删除集合中的文件描述符;
fd:要操作的文件描述符;
event:文件描述符对应的事件;
EPOLLIN:读事件;
EPOLLOUT:写事件;
data.fd:关注的文件描述符;
返回值:
成功:0;
失败:-1
epoll_wait
功能:通知内核开始进行监测;
参数:
epfd:监测的文件描述符集合;
event:保存返回的到达事件的结果;
struct epoll_event evs[MAX_FD_CNT]
evs
maxevents:最大的事件个数;
timeout:监测的超时时间;
-1:不设置超时(阻塞);
返回值:
成功:到达事件的个数;
失败:-1
select特点:
1.使用位图对文件描述符的集合进行保存,允许检测1024个文件描述符,有监测限制;
2.需要应用层和内核层的复述与拷贝;
3.返回的集合表需要遍历寻找到达的事件;
4.只能工作在水平触发模式(低速模式),不能工作在边沿触发模式(高速模式);
poll特点:
1.使用链表实现对文件描述符集合的保存,没有了监测的文件描述符上限的限制;
2.需要应用层和内核层的复述与拷贝;
3.返回的集合表需要遍历寻找到达的事件;
4.只能工作在水平触发模式(低速模式),不能工作在边沿触发模式(高速模式);
epoll特点:
1.使用红黑树(特殊二叉树)实现文件描述符集合的存储,没有文件米哦啊舒服上限限制,从而提 高查找效率;
2.将文件描述符集合创建在内核层,避免了应用层和内核层的反复数据拷贝;
3.返回的是到达事件,无需遍历,只需处理事件;
4.可工作在水平触发模式,也可工作在边沿触发模式;