目录
服务器模型
1.循环服务器
一次只有一个客户端可以连接,但是客户端退出以后,下一个客户端可以继续连接
2.并发服务器
同时一时刻可以连接多个客户端,常见:IO多路复用(select、poll、epoll)、多进程、多线程
(1)多进程模型
多进程特点
1.fork之前的代码被复制,但是不会重新执行一遍;fork之后的代码被复制,并且再被执行一遍。
2.fork之后两个进程相互独立,子进程拷贝了父进程的所有代码,但内存空间独立
3.fork之前打开文件,fork之后拿到的是同一个文件描述符,操作的是同一个文件指针
多进程案例
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/select.h>
#include <signal.h>
#include <wait.h>
#define N 64
char buf[N];
pid_t pid;
void father(int sig)
{
waitpid(-1, NULL, WNOHANG); // 回收子进程资源
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(saddr);
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("bind失败");
close(sockfd);
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
perror("lisren失败");
close(sockfd);
return -1;
}
printf("listen成功\n");
while (1)
{
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
perror("accept失败");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d 已连接\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pid = fork();
if (pid == 0)
{
// close(sockfd);
while (1)
{
memset(buf, 0, N);
int ret = recv(acceptfd, buf, N, 0);
if (ret < 0)
{
perror("recv失败\n");
break;
}
else if (ret == 0)
{
printf("客户端ip:%s退出\n", inet_ntoa(caddr.sin_addr));
break;
}
else
{
printf("%s客户端: %s\n", inet_ntoa(caddr.sin_addr), buf);
}
}
close(acceptfd);
exit(0);
}
else
{
close(acceptfd);
signal(SIGCHLD, father);
}
}
return 0;
}
(2)多线程模型
多线程特点
每来一个连接创建一个线程,IO多路复用以外,应用最广泛的并发服务器模型
多线程案例
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#define N 64
void *mythread(void *arg)
{
pthread_detach(pthread_self());
int fd = *((int *)arg);
free(arg);
char buf[N];
while (1)
{
memset(buf, 0, N);
int ret = recv(fd, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
break;
}
else if (ret == 0)
{
printf("客户端acceptfd:%d 退出\n", fd);
close(fd);
break;
}
else
{
printf("客户端: %s\n", buf);
}
}
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:%s <port>\n", argv[0]);
return -1;
}
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
{
perror("socket失败");
return -1;
}
printf("sockfd:%d\n", sockfd);
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(saddr);
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("bind失败");
close(sockfd);
return -1;
}
printf("bind成功\n");
if (listen(sockfd, 7) < 0)
{
perror("listen失败");
close(sockfd);
return -1;
}
printf("listen成功\n");
while (1)
{
int *acceptfd = malloc(sizeof(int)); // 为每个客户端连接分配内存
if (acceptfd == NULL)
{
perror("内存分配失败");
close(sockfd);
return -1;
}
*acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (*acceptfd < 0)
{
perror("accept失败");
free(acceptfd);
continue;
}
printf("acceptfd: %d\n", *acceptfd);
printf("客户端ip:%s 端口号:%d 连接\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
pthread_t tid;
if (pthread_create(&tid, NULL, mythread, acceptfd) != 0)
{
perror("创建线程失败");
close(*acceptfd);
free(acceptfd);
continue;
}
}
close(sockfd);
return 0;
}
网络超时检测
在网络通信中,很多操作会使得进程阻塞:
TCP套接字中的recv/accept
UDP套接字中的recvfrom
超时检测的必要性
○避免进程在没有数据时无限制地阻塞
○实现某些特定协议要求,比如某些设备规定,发送请求数据后,如果多长时间后没有收到来自设备的回复,需要做出一些特殊处理
网络超时检测方法
1.自带超时参数的函数
例如使用select/poll/epoll函数最后一个参数可以设置超时。
(1)select设置超时
struct timeval tm = {2, 0};//设置2s打算阻塞
sret = select(maxfd + 1, &tempfds, NULL, NULL, &tm);
第五个参数:
struct timeval
{
long tv_sec; /*秒*/
long tv_usec; /*微秒*/
};
注:select的超时检测不会更新,所以每次重新超时等待之前要更新等待时间
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/select.h>
#define N 64
char buf[N];
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
// saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
#if 0
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
saddr.sin_addr.s_addr = INADDR_ANY;
#endif
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
ERR_MSG("bind失败");
close(sockfd);
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
close(sockfd);
return -1;
}
printf("listen成功\n");
// 第一步:建表初始化
fd_set readfds, tempfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
FD_SET(0, &readfds);
int max = sockfd;
while (1)
{
struct timeval tm = {2, 0};
memset(buf, 0, N);
tempfds = readfds;
int ret = select(max + 1, &tempfds, NULL, NULL, &tm);
if (ret == 0)
{
printf("time out\n");
}
if (FD_ISSET(sockfd, &tempfds))
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
ERR_MSG("accept失败\n");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
FD_SET(acceptfd, &readfds);
if (max < acceptfd)
{
max = acceptfd;
}
}
// 5.发送
else if (FD_ISSET(0, &tempfds))
{
scanf("%s", buf);
for (int i = 4; i <= max; i++)
{
if (FD_ISSET(i, &readfds))
{
send(i, buf, N, 0);
}
}
}
// 6.接收
for (int n = 4; n <= max; n++)
{
if (FD_ISSET(n, &tempfds))
{
int ret = recv(n, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
close(sockfd);
return -1;
}
else if (ret > 0)
{
printf("客户端acceptfd%d:%s\n", n, buf);
}
else
{
printf("客户端acceptfd:%d退出\n", n);
FD_CLR(n, &readfds);
close(n);
while (!FD_ISSET(max, &readfds))
{
max--;
}
}
}
}
}
close(sockfd);
return 0;
}
(2)poll设置超时
直接在poll(fds, last + 1, 2000);设置,延迟2s;
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <poll.h>
#define N 64
char buf[N];
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
// #if 0
// saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
// #else
// saddr.sin_addr.s_addr = INADDR_ANY;
// #endif
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
ERR_MSG("bind失败");
close(sockfd);
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
close(sockfd);
return -1;
}
printf("listen成功\n");
// 第一步:建表初始化
struct pollfd fds[100];
int last = -1;
// 第二步:填表
fds[++last].fd = 0;
fds[last].events = POLLIN;
fds[last].revents = 0;
fds[++last].fd = sockfd;
fds[last].events = POLLIN;
fds[last].revents = 0;
while (1)
{
memset(buf, 0, N);
int po = poll(fds, last + 1, 2000);
if (po == -1)
{
perror("select失败");
close(sockfd);
return -1;
}
for (int i = 0; i <= last; i++)
{
if (fds[i].revents == POLLIN)
{
if (fds[i].fd == sockfd)
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
ERR_MSG("accept失败\n");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
fds[++last].fd = acceptfd;
fds[last].events = POLLIN;
fds[last].revents = 0;
}
else if (fds[i].fd == 0)
{
scanf("%s", buf);
for (int j = 2; j <= last; j++)
{
send(fds[j].fd, buf, N, 0);
}
}
else
{
int ret = recv(fds[i].fd, buf, N, 0);
if (ret < 0)
{
perror("recv失败");
close(sockfd);
return -1;
}
else if (ret > 0)
{
printf("客户端%s:%s\n", inet_ntoa(caddr.sin_addr), buf);
}
else
{
printf("客户端acceptfd:%d退出\n", fds[i].fd);
close(fds[i].fd);
fds[i] = fds[last];
last--;
}
}
}
}
}
close(sockfd);
return 0;
}
2.利用setsockopt属性设置
Linux中socket属性
API接口
功能:设置/获取网络属性;
#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);参数:
int sockfd:指定要设置/获取哪个套接字的属性;
int level:指定要控制的协议层次;
SOL_SOCKET:应用层 通用套接字选项; man 7 socket
IPPROTO_TCP:TCP选项 man 7 TCP
IPPROTO_UDP:UDP选项 man 7 UDP
IPPROTO_IP:IP选项; man 7 IP
int optname:指定要控制的内容,指定控制方式;
--- SOL_SOCKET: man 7 socket -----
SO_REUSEADDR:允许端口快速重用 optval: int*
SO_BROADCAST:允许广播 optval: int*
SO_RCVBUF/SO_SNDBUF:接收缓冲区 发送缓冲区大小
SO_RCVTIMEO/SO_SNDTIMEO:接收超时时间,发送超时时间
void *optval:根据optname不同,该类型不同;
socklen_t optlen/socklen_t *optlen:真实的optval指针指向的内存空间的大小;
返回值:
成功:返回0
失败:返回-1
设置sockfd接收超时
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
// saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
#if 0
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
saddr.sin_addr.s_addr = INADDR_ANY;
#endif
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("bind失败");
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
return -1;
}
printf("listen成功\n");
struct timeval val = {2, 0};
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &val, sizeof(val));
while (1)
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
if (errno == 11)
{
char a[10];
printf("accept超时,是否继续,继续请输入yes,退出请输入no\n");
scanf(" %s", a);
if (strcmp(a, "yes") == 0)
{
continue;
}
if (strcmp(a, "no") == 0)
{
break;
}
else
{
printf("输入错误,请重新输入\n");
continue;
}
}
perror("accept失败");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 5.发送
#define N 64
char buf[N];
while (1)
{
memset(buf, 0, N);
int ret = recv(acceptfd, buf, N, 0); // MSG_DONTWAIT
if (ret < 0)
{
if (errno == 11)
{
printf("读缓存区内没数据\n");
continue;
}
else
{
perror("recv失败\n");
return -1;
}
}
else if (ret == 0)
{
printf("客户端ip:%s退出\n", inet_ntoa(caddr.sin_addr));
close(acceptfd);
break;
}
else
{
printf("客户端%s\n", buf);
}
}
}
close(sockfd);
return 0;
}
设置端口重用
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#define ERR_MSG(msg) \
do \
{ \
fprintf(stderr, "line:%d ", __LINE__); \
perror(msg); \
} while (0)
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("用法:<port>\n");
return -1;
}
// 1.创建套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
printf("sorkfd:%d\n", sockfd);
// 2.bind绑定IP和Port端口号
struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
// saddr.sin_addr.s_addr = inet_addr("192.168.50.213");
socklen_t addrlen = sizeof(saddr);
#if 0
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
#else
saddr.sin_addr.s_addr = INADDR_ANY;
#endif
int flag;
socklen_t len = sizeof(flag);
getsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, &len);
flag = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &flag, len);
printf("flag:%d\n", flag);
if (bind(sockfd, (struct sockaddr *)&saddr, addrlen) < 0)
{
perror("bind失败");
return -1;
}
printf("bind成功\n");
// 3.监听listen将主动套接字变为被动套接字
if (listen(sockfd, 7) < 0)
{
ERR_MSG("lisren失败");
return -1;
}
printf("listen成功\n");
while (1)
{
// 4.accept阻塞等待链接
int acceptfd = accept(sockfd, (struct sockaddr *)&caddr, &addrlen);
if (acceptfd < 0)
{
perror("accept失败");
return -1;
}
printf("acceptfd:%d\n", acceptfd);
printf("客户端ip:%s\t 端口号:%d\n", inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
// 5.发送
#define N 64
char buf[N];
while (1)
{
memset(buf, 0, N);
int ret = recv(acceptfd, buf, N, 0); // MSG_DONTWAIT
if (ret < 0)
{
if (errno == 11)
{
printf("读缓存区内没数据\n");
continue;
}
else
{
perror("recv失败\n");
return -1;
}
}
else if (ret == 0)
{
printf("客户端ip:%s退出\n", inet_ntoa(caddr.sin_addr));
close(acceptfd);
break;
}
else
{
printf("客户端%s\n", buf);
}
}
}
close(sockfd);
return 0;
}