TCP UDP协议的应用以及高级IO的介绍+
网络通信协议
模型:
- TCP和UDP两个协议都是一对多的网络通信模型
- TCP编程模型
实例:
TCP模型
聊天室的服务器:
有私密消息功能以及列出聊天者的功能
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_CNT 100
#define MSG_LEN 1024
#define NAME_LEN 48
struct Client{
char name[NAME_LEN];
int fd;
struct sockaddr_in addr;
pthread_t id;
};
struct Client clts[MAX_CNT] = {};
size_t cnt = 0;
pthread_mutex_t lock;
int sockfd;
#define LOG_ERROR(fmt, args...)\
fprintf(stderr, "[ERROR [%s:%d]\n"fmt,__func__,__LINE__,##args);
void handleExit(int sig){
close(sockfd);
int i;
for(i = 0; i < cnt; i++){
pthread_cancel(clts[i].id);
}
for(i = 0; i < cnt; i++){
pthread_join(clts[i].id, NULL);
}
}
int init_server(const char *ip, unsigned short port){
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
LOG_ERROR("socket:%s\n", strerror(errno));
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
if(bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1){
LOG_ERROR("bind:%s\n", strerror(errno));
return -1;
}
if(listen(sockfd, MAX_CNT) == -1){
LOG_ERROR("listen:%s\n", strerror(errno));
return -1;
}
return 0;
}
void broadcast(int fd, const char *msg){
for(int i = 0; i < cnt; i++){
if(fd == clts[i].fd){
continue;}
send(clts[i].fd, msg, strlen(msg)+1,0);
}
}
void delClt(int fd){
pthread_mutex_lock(&lock);
for(int i = 0; i < cnt; i++){
if(clts[i].fd == fd){
clts[i] = clts[cnt-1];
--cnt;
break;
}
}
pthread_mutex_unlock(&lock);
}
void handlePrivateMsg(char *buf, const char *name){
int fd = 0;
sscanf(buf, "%d", &fd);
char msg[MSG_LEN] = {};
while(*buf != ' ' && *buf != '\0') ++buf;
if(*buf == '\0'){
strcpy(msg,name);
strcat(msg, "拍了拍我");
}
else{
strcpy(msg, name);
strcat(msg, " ");
strcat(msg, "四米消息:");
strcat(msg, buf);
}
send(fd, msg, strlen(msg)+1, 0);
}
void handldList(int fd){
char msg[MSG_LEN] = {};
sprintf(msg, "id:name\r\n");
int i = 0;
for(i = 0; i < cnt; ++i){
char buf[128] = {};
if(clts[i].fd == fd){
sprintf(buf, "%d:%s[自己]\r\n", clts[i].fd, clts[i].name);
}
else{
sprintf(buf, "%d:%s\r\n", clts[i].fd, clts[i].name);
}
strcat(msg, buf);
}
if(i == 0){
strcpy(msg, "it's empty!\r\n");
}
send(fd, msg, strlen(msg)+1, 0);
}
void *handleClient(void *arg)
{
struct Client clt = *(struct Client *)arg;
ssize_t rb = recv(clt.fd, clt.name, NAME_LEN, 0);
if(rb <0){
LOG_ERROR("recv:%s\n",strerror(errno));
return NULL;
}
char buf[NAME_LEN] = {};
strcpy(buf, clt.name);
strcat(buf, " ");
strcat(buf, "进入聊天室,真是太帅辣!");
broadcast(clt.fd, buf);
pthread_mutex_lock(&lock);
clts[cnt++] = clt;
pthread_mutex_unlock(&lock);
int len = strlen(clt.name);
for(;;){
strcpy(buf,clt.name);
strcat(buf,":");
rb = recv(clt.fd, buf+len+1, MSG_LEN-len-1, 0);
if(rb < 0){
LOG_ERROR("recv:%s\n",strerror(errno));
delClt(clt.fd);
break;
}
if(rb == 0){
strcpy(buf, clt.name);
strcat(buf, " ");
strcat(buf, "退出了聊天室");
delClt(clt.fd);
broadcast(clt.fd, buf);
break;
}
if(strncmp(buf+len+1, "@", 1) == 0){
handlePrivateMsg(buf+len+2, clt.name);
}
else if(strncmp(buf+len+1, "!list", 5) == 0){
handldList(clt.fd);
}
else{
broadcast(clt.fd, buf);
}
}
return NULL;
}
void server_run(const char *ip, unsigned short port){
if(signal(SIGINT, handleExit) == SIG_ERR)
{
LOG_ERROR("signal:%s\n",strerror(errno));
return;
}
if(init_server(ip, port) == -1){
return;
}
struct Client clt = {};
socklen_t addrlen = sizeof(clt.addr);
for(;;){
clt.fd = accept(sockfd, (struct sockaddr*)&clt.addr, &addrlen);
if(clt.fd == -1)
{
LOG_ERROR("accept:%s\n",strerror(errno));
exit(-1);
}
errno = pthread_create(&clt.id, NULL, handleClient, &clt);
if(errno != 0){
LOG_ERROR("pthread_create:%s\n", strerror(errno));
}
else{
LOG_ERROR("%s[%hu] client connected!\n",inet_ntoa(clt.addr.sin_addr),ntohs(clt.addr.sin_port));
}
}
}
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法 :%s <ip> <port>\n", argv[0]);
return -1;
}
server_run(argv[1], atoi(argv[2]));
return 0;
}
聊天室客户端
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
int sockfd;
int connect_server(const char* ip, unsigned short port){
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip);
int cnt = 0;
while(connect(sockfd, (struct sockaddr*)&addr, sizeof(addr)) == -1){
perror("connect");
if(errno == ECONNREFUSED || errno == EAGAIN){
++cnt;
if(cnt == 3)
return -1;
}
}
return 0;
}
void *recvData(void *arg){
char buf[1024] = {};
for(;;){
ssize_t rb = recv(sockfd, buf, sizeof(buf), 0);
if(rb <= 0){
break;
}
printf("\r%s\n>",buf);
fflush(stdout);
}
return NULL;
}
void *sendData(void *arg){
char buf[1024] = {};
scanf("%*[^\n]");
scanf("%*c");
for(;;){
printf(">");
fgets(buf, sizeof(buf), stdin);
int len = strlen(buf);
if(buf[len-1] == '\n'){
len--;
buf[len] = '\0';
}
if(len > 0){
ssize_t wb = send(sockfd, buf, len+1, 0);
if(wb <= 0){
perror("send");
break;
}
}
}
return NULL;
}
void handleExit(){
close(sockfd);
exit(0);
}
void client_run(const char *ip, unsigned short port){
if(signal(SIGINT, handleExit) == SIG_ERR){
perror("signal");
return;
}
if(connect_server(ip, port) == -1){
return;
}
char name[48] = {};
printf("input your name\n");
scanf("%s", name);
ssize_t wb = send(sockfd, name, strlen(name)+1, 0);
pthread_t id;
int err = pthread_create(&id, NULL, recvData, NULL);
sendData(NULL);
}
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法:%s <ip> <port>\n", argv[0]);
return -1;
}
client_run(argv[1], atoi(argv[2]));
return 0;
}
UDP模型
服务器
#include <time.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("用法; %s <ip> <port>\n", argv[0]);
return -1;
}
printf("1.udp服务器:创建socket套接字...\n");
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("2.upd服务器:绑定到明确的ip和port通信地址上...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int ret = bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1){
perror("bind");
return -1;
}
printf("3.udp服务器:循环接受用户信息...\n");
for(;;){
struct sockaddr_in caddr;
socklen_t addrlen = sizeof(caddr);
char buf[1024] = {};
ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&caddr, &addrlen);
printf("recv:%s(ip:%s port:%hu)\n",buf, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
time_t t = time(NULL);
char *pt = ctime(&t);
ret = sendto(sockfd, pt, strlen(pt)+1, 0, (struct sockaddr*)&caddr, addrlen);
if(ret < 0){
perror("sendto");
return -1;
}
}
close(sockfd);
return 0;
}
客户端
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main(int argc,const char* argv[])
{
if(argc < 3){
printf("%s <ip> <port>", argv[0]);
return -1;
}
printf("1.udp客户端:创建socket套接字...\n");
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd == -1){
perror("socket");
return -1;
}
printf("2.udp客户端:准备服务器的通信地址...\n");
struct sockaddr_in saddr;
memset(&saddr, 0, sizeof(saddr));
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2]));
saddr.sin_addr.s_addr = inet_addr(argv[1]);
socklen_t addrlen = sizeof(saddr);
printf("3.udp客户端:循环发送数据给服务器...\n");
for(;;){
char buf[1024] = {};
fgets(buf, sizeof(buf), stdin);
if(strncmp(buf, "!quit", 5) == 0){
break;
}
int ret = sendto(sockfd, buf, strlen(buf)+1, 0, (struct sockaddr*)&saddr, addrlen);
if(ret <= 0){
perror("sendto");
break;
}
ret = recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("recv%s form server\n", buf);
}
close(sockfd);
return 0;
}
区别:
TCP
-
Transmission Control Protocol 传输控制协议 -
面向连接 (客户端需要调用connect进行连接,三次握手) -
可靠 数据传输保证数据的完整性和有序性 数据检验、超时自动重传、丢失重传、滑动窗口机制(保证数据收发一致)应答机制 A B C D E —> ABCDE -
传输效率比较慢 -
安全性要高
UDP
TCP UDP协议报头
- TCP头部
-
- 16位源端口 16为目的地址端口
- 点到点一台主机上的进程发送到另一台主机上
- 32位的序列号,TCP数据报的编号, 没法送一个编号自动加一, 32为的确认序列号,每收到一个+1
- 4位首部长度 TCP首部最少20字节,最多60字节 以4字节为单位(TCP首部长度一定是4的整数倍) TCP首部长度 = 首部长度数值X4 字节
- URG、ACK、PSH、RST、SYN、FIN是六个控制位
* URG:紧急标志位(The urgent pointer),说明紧急指针有效。
* ACK:确认标志位(Acknowledgement Number),大多数情况下该标志位是置位的,说明确认序列号有效。该标志在TCP连接的大部分时候都有效。
* PSH:推(PUSH)标志位,该标志置位时,接收端在收到数据后应立即请求将数据递交给应用程序,而不是将它缓冲起来直到缓冲区接收满为止。在处理telnet或rlogin等交互模式的连接时,该标志总是置位的。
* RST:复位标志,用于重置一个已经混乱(可能由于主机崩溃或其他的原因)的连接。该位也可以被用来拒绝一个无效的数据段,或者拒绝一个连接请求。
* SYN:同步标志,说明序列号有效。该标志仅在三次握手建立TCP连接时有效。它提示TCP连接的服务端检查序列号,该序列编号为TCP连接初始端(一般是客户端)的初始序列编号。
* FIN:结束标志,带有该标志置位的数据包用来结束一个TCP会话,但对应端口仍处于开放状态,准备接收后续数据。在TCP四次断开时会使用这个标志位。
- 16位窗口大小 滑动窗口机制 为了限制传输速度
- 16位检验和
- 16位紧急指针 URGUDP报文
- 16位原端口 16位目标地址端口
-
UDP报头
- 16位源端口 16位目的地端口 点到点
- 16位UDP长度
- 16位UDP检验和
TCP | UDP |
---|
Transmission Control Protocol 传输控制协议 | User Datagram Protocol 用户数据报文协议 | 面向连接(三次握手四次分手) | 无连接 | 可靠、安全、保证数据有序 | 不可靠、不安全、数据可能丢失、顺序不确定 | 延时重传、丢失重传、应答、检验、滑动窗口 | 没有重传、没有检验、没有应答 | 复杂、传输效率稍低 | 简单、高效、传输速度快 | 适合场合:安全性高、数据量少 | 适合场景:视频传输、数据量大的情况、对数据安全性要求不高 | SOCK_STREAM | SOCK_DGRAM | socket/bind/listen/accept/recv/send/connect/close | socket/bind/recvfrom/sendto/close |
- UDP可以实现可靠的数据传输吗?
- 可以
- 怎么实现:在使用udp时,在应用层实现检验、应答、重传等机制
套接字选项
#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname,void *optval, socklen_t *optlen);
int setsockopt(int sockfd, int level, int optname,const void *optval, socklen_t optlen);
- 可以设置/获取的选项
- 通用选项,工作在套接字类型上
- 在套接字层次管理的选项,但是依赖于下层协议的支持
- 特定用于某协议的选项,某个协议独有
- 参数level标识了应用的协议
- 如果是通用的套接字层次选项 SOL_SOCKET
- 否则level设置成控制这个选项的协议编号
- TCP选项 IPPROTO_TCP
- IP选项 IPPROTO_IP
optname | optval类型 | 描述 |
---|
SO_ACCEPTCONN | int | 返回信息指示该套接字是否能被监听(仅getsockopt) 是否能调用listen | SO_BROADCAST | int | 如果*optval非0,广播数据报 | SO_DEBUG | int | 如果*optval非0,启用网络驱动调试功能 | SO_DONTROUTE | int | 如果*optval非0,绕过通常路由 | SO_ERROR | int | 返回挂起的套接字错误并清除(仅getsockopt) | SO_KEEPALIVE | int | 如果*optval非0,启用周期性keep-alive报文 | SO_LINGER | struct linger | 当还未发报文而套接字已关闭时,延迟时间 | SO_OOBINLINE | int | 如果*optval非0,将带外数据放在普通数据中 | SO_RCVBUF | int | 接收缓冲区的字节长度 | SO_RCVLOWAT | int | 接收调用中返回的最小数据字节数 | SO_RCVTIMEO | struct timeval | 套接字接收调用的超时值 | SO_REUSEADDR | int | 如果*optval非0,重用bind中的地址 | SO_SNDBUF | int | 发送缓冲区字节长度 | SO_SNDLOWAT | int | 发送调用中传送的最小数据字节数 | SO_SNDTIMEO | struct timeval | 套接字发送调用的超时值 | SO_TYPE | int | 标识套接字类型(仅getsockopt) |
- 做一个测试,首先启动server,然后启动client,用Ctrl-C终止server,马上再运行server,运行结果:
bind error: Address already in use
-
erver终止时,socket描述符会自动关闭并发FIN段给client,client收到FIN后处于CLOSE_WAIT状态,但是client并没有终止,也没有关闭socket描述符,因此不会发FIN给server,因此server的TCP连接处于FIN_WAIT2状态 -
client终止时自动关闭socket描述符,server的TCP连接收到client发的FIN段后处于TIME_WAIT状态。TCP协议规定,主动关闭连接的一方要处于TIME_WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态,因为我们先Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口。 MSL在RFC 1122中规定为两分钟,但是各操作系统的实现不同,在Linux上一般经过半分钟后就可以再次启动server了 -
端口复用 int opt = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
-
心跳检测机制
- 方式一:SO_KEEPALIVE 用来检测非正常断开
- 方式二:写一个守护进程,定时发送Heart-Beat包,用于检测对方是否在线
带外数据
-
区别于普通数据,可以优先处理紧急数据 -
当有紧急数据时,内部会为该进程递送一个SIGURG信号,如果需要处理带外数据, 需要实现注册SIGURG信号的处理函数, 直接或者简介去接受带外数据 -
signal(SIGURG,handleurg);
-
建立socket套接字的所有权,以确保信号可以被地送到合适的进程- -
fcntl(sockfd, F_SETOWN, getpid());
-
recv/send在发送和接收带外数据时, 可以指定flag为MSG_OOB -
TCP头v不URG的标识,以及一个16位 的紧急指针
- 紧急数据只有一个字节, 只会把tcp首部的紧急指针的前一个字节当作紧急数据
- 如果多次接受到多次紧急数据,会把前面的紧急数据丢弃
-
如果接收端请求读取带外数据(recv指定MSG_OOB),但是没有带外数据,则recv将出错并设置errno位EINVAL -
在一个接受进程中, 被告知有带外数据的前提下,但是读取带外数据时,带外数据还没有到达,如果使用非阻塞的读取则直接返回-1并设置errno位EOWULDBLOOK -
如果接收进程已经设置了套接字选项SO_OOBINLINE,则将带外数据作为普通数据读取,此时如果试图用MSG_OOB标识标志读取带外数据,则返回-1,且设置errno位EINVAL -
带外数据不会受到流量控制,会确保能够正确的发送,在接受时带外数据拥有独立缓冲区,即使接收缓冲区已满,带外缓冲区仍然可以也能正常读取
带外标记
#include <sys/socket.h>
int sockatmark(int sockfd);
-
每当收到一个带外数据时,就有一个与之关联的带外标记 -
在从套接字读入期间,接收进程可以通过sockatmark函数确认是否处于带外标记 -
可以通过SO_OOBINLINE这样的方式读取带外数据,
高级IO
阻塞IO/非阻塞IO
同步IO/异步IO
散布读/聚集写
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
size_t recvmsg(int sockfd, struct msghdr *msg, int flags);
函数 | 任何描述符 | 仅套接字描述符 | 单个缓冲区 | 分散/集中读写 | 是否可选标志 | 可选对端地址 | 可选控制 信息 |
---|
read/write | OK | | OK | | | | | readv/writev | OK | | | OK | | | | recv/send | | OK | OK | | OK | | | recvfrom/sendto | | OK | OK | | OK | OK | | recvmsg/sendmsg | | OK | | OK | OK | OK | OK |
多路复用IO
-
在使用线程模型开发服务器时需考虑以下问题:
- 1.调整进程内最大文件描述符上限 头文件中定义的一个宏
- 2.线程如有共享数据,考虑线程同步
- 3.服务于客户端线程退出时,退出处理。(退出值,分离态)
- 4.系统负载,随着链接客户端增加,导致其它线程不能及时得到CPU
-
使用多进程和多线程实现服务器会有瓶颈
-
多路复用IO
- 开多线程的目的是为了一个线程监视一个客户端(去读取和响应客户端的请求)
- 多路复用IO可以让内核监视所有的客户端(文件描述符),并且通过一定的方式告诉用户,有哪些客户端发来了请求和数据
select/pselect
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
struct timeval {
long tv_sec;
long tv_usec;
};
struct timespec {
long tv_sec;
long tv_nsec;
};
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);
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);
-
select的编程模型
- 轮询的机制 一直在调用select函数
- 第一步:把关心的文件描述符添加到对应的文件描述符集合中 (循环) 找出最大的文件描述符
- 第二步: 调用select函数
- 需要提前保存文件描述符集合
- select函数在内核运行中,去遍历文件描述符符合,测试每一个文件描述符是否可读、可写、巩异常 (循环0-maxfd) 把没有可读、可写、异常文件描述符从对应的文件描述符集合中删除
- 第三步:遍历测试FD_ISSET关心的文件描述符是否还在可读、可写、异常的文件描述符集合中 (循环)
-
select的缺点
- 随着文件描述符(客户端)的增加,效率急剧下降
- 解决1024以下客户端时使用select是很合适的,但如果链接客户端过多,select采用的是轮询模型,会大大降低服务器响应效率
- select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数
poll/ppoll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd;
short events;
short revents;
};
#define _GNU_SOURCE
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,const struct timespec *tmo_p, const sigset_t *sigmask);
-
编程模型
- 把初始的文件描述符和监听的事件放到struct pollfd数组中
- 轮询的机制
- 调用poll函数 (需要遍历struct pollfd数组)
- 处理文件描述符的事件 循环遍历struct pollfd数组中所有成员,判断其返回事件中revents是否有关心的事件发生,如果则去处理 (有客户端连接 struct pollfd数组中添加成员,如果有退出删除其值)
- 如果不再监控某个文件描述符时,可以把pollfd中,fd设置为-1,poll不再监控此pollfd,下次返回时,把revents设置为0
-
poll相对于select而言,效率提高了
- 复杂的文件描述符集合的操作
- 不需要每次都像select一样把文件文件符重新组装
- poll返回事件和监听事件分开
epoll
- linux下独有的 efficient高效的poll机制
- 非常适用于文件描述符数量巨大且只有少量处于活跃状态的场景
- 高并发首选epoll
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events;
epoll_data_t data;
};
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,int maxevents, int timeout,const sigset_t *sigmask);
|