服务端代码
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
getchar();
}
运行该段代码,可以使用netstat -anop | grep 9999查看某一个端口(如:9999)进程的命令。结果如下图。可以发现代码执行到此处的时候,程序已经开始监听了。
可以使用第三方的网络助手工具尝试连接该端程序。可以发现可以正常发送成功,但是却没有没有反馈。
出现上述问题主要原因见下图。通过listen这是监听了,并没有真正的建立连接。建立连接是通过accept来实现的,并且每个客户端都有一个服务对应处理。
故而添加accept代码如下
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
//sleep(10);
#if 0
printf("sleep\n");
int flags = fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flags);
#endif
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
// getchar();
}
上述代码运行效果如下
可以发现代码阻塞在accept函数处,此时通过工具建立连接,就会继续运行。而通过代码中的#if…#endif处的代码可以将阻塞io转换为非阻塞io。
思考:若在listen之后还未到accept的时候建立连接会成功吗?修改代码验证该思考。
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
sleep(10);
printf("sleep\n");
#if 0
printf("sleep\n");
int flags = fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flags);
#endif
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
// getchar();
}
使用工具在未打印sleep之前点击连接,过一会儿运行结果如下图,可以发现依然可以建立连接。
所以该思考的结果是:accept和是否能建立连接没有关系。如在listen之后sleep(10),还没有到accept的时候建立连接也是可以的。
思考:将代码改成非阻塞的,若在accept到来之前还未点击连接会如何?修改案例代码如下:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
sleep(10);
#if 1
printf("sleep\n");
int flags = fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(sockfd, F_SETFL, flags);
#endif
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
// getchar();
}
代码运行结果:
可以发现失败了,linux下正数表示成功,-1表示失败。而如果在运行到accept之前点击了连接,就会连接成功。此处从3开始,是因为0是标准输入,1是标准输出,2是错误。
上面只实现了网络io的连接,那么接下来考虑服务端如何接收数据。使用recv,recv返回0表示断开连接
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define BUFFER_LENGTH 1024
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
printf("ret: %d, buffer: %s\n", ret, buffer);
send(clientfd, buffer, ret, 0);
// getchar();
}
使用第三方的网络助手工具尝试连接该端程序,代码运行结果如下:
通过上图可以发现recv也是阻塞的,只会阻塞一次,send 也是一次**。**
思考:那么放入到while中是否可以发送多次数据呢?代码如下:
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define BUFFER_LENGTH 1024
int main() {
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr))) {
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
while (1) { //slave
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
close(clientfd);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(clientfd, buffer, ret, 0);
}
// getchar();
}
代码运行结果如下:
可以发现支持数据的多次接收。那么这段代码是否可以接收多个客户端的请求呢。实例如下:
可以发现无法处理第二个客户端的请求,思考后,这是因为accept只有一次,那么将accept放入到while中是否可以呢?
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#define BUFFER_LENGTH 1024
int main()
{
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
while (1)
{ //slave
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0)
{
close(clientfd);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(clientfd, buffer, ret, 0);
}
// getchar();
}
代码运行结果:
可以发现只能发送一次,这是因为第二次发送的时候调用了accept,此时没有连接,会被阻塞住。那么为了在while中即调用accept,又可以调用recv,所以考虑使用多线程实现。代码如下所示。该段代码编译需要使用gcc -o xxx xxx.c -lpthread.
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <pthread.h>
#define BUFFER_LENGTH 1024
void *client_thread(void *arg)
{
int clientfd = *(int*)arg;
while (1)
{ //slave
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if (ret == 0) {
close(clientfd);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(clientfd, buffer, ret, 0);
}
}
int main()
{
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
while (1)
{ //slave
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("sockfd:%d, clientfd:%d\n", sockfd, clientfd);
pthread_t threadid;
pthread_create(&threadid, NULL, client_thread, &clientfd);
}
// getchar();
}
代码运行结果:
从上图结果可以发现可以达到我们的预期目标。但是在客户端多的时候,比如有10000个客户端,无法创建10000个线程。那么如何处理这个问题。此时就需要用到本章的重点知识,网络io多路复用技术,对多个服务进行管理。如select, pool, epool,kqueue(mac)等。
网络IO复用是指在单线程或少数线程的情况下,通过一种机制同时监控多个IO流的状态,当某个IO流有数据到达时,就通知相应的线程进行处理。其中,select是一种比较常用的IO多路复用技术,它可以同时监控多个文件描述符,当某个文件描述符就绪(一般是读就绪或写就绪)时,就会通知应用程序进行相应的操作。
代码
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/select.h>
#define BUFFER_LENGTH 1024
int main()
{
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
fd_set rfds, rset;
FD_ZERO(&rfds);
FD_SET(sockfd, &rfds);
int maxfd = sockfd;
int clientfd = 0;
while (1)
{
rset = rfds;
int nready = select(maxfd+1, &rset, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &rset))
{
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept: %d\n", clientfd);
FD_SET(clientfd, &rfds);
if (clientfd > maxfd) maxfd = clientfd; // 这是因为有回收机制,所以始终找最大值。
if (-- nready == 0) continue;
}
int i = 0;
for (i = sockfd + 1; i <= maxfd; i++) {
if (FD_ISSET(i, &rset))
{
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(i, buffer, BUFFER_LENGTH, 0);
if (ret == 0)
{
close(i);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(i, buffer, ret, 0);
}
}
}
// getchar();
}
运行结果
发现可以达到同样的目的。
代码说明
select(maxfd, &rfds, &wfds, efds, timeout)
maxfd - 表示的是所有的accept连接中返回值最大的id,
rfds - 可读的集合,记录了可以读的io集合
wfds - 可写的集合,记录了可以写的io集合
efds - 出错的集合,记录了上次出错的io集合
timeout - 表示多久轮询上面三个集合一次
返回值 - 表示当前有多少个io连接
fd_set rfds: fd_set内部是按照bit位来的
FD_ZERO(&rfds): 是设置fd_set中的每一个bit位为0
FD_SET(x, &rfds):将rfds中的dix位置为1
FD_ISSET(socked, &rfds):表示查询rfds的第socked位是否为1
关于send是否可以写的问题,是应用将需要发送的数据放入到内核的sendbuffer中。通常而言都是可以send成功的,只有在循环send或sendbuffer()非常小的时候才会失败,这个配置可以在sysconfig文件中修改,所以send是否可以写是需要判断的。
poll是一种常见的IO多路复用技术,它可以同时监视多个文件描述符,当其中任意一个文件描述符就绪时,就会通知应用程序进行相应的操作。
代码
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/poll.h>
#define BUFFER_LENGTH 1024
#define POLL_SIZE 1024
int main()
{
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct pollfd fds[POLL_SIZE] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN; // 表示可读
int maxfd = sockfd;
int clientfd = 0;
while (1) {
int nready = poll(fds, maxfd + 1, -1);
if (fds[sockfd].revents & POLLIN)
{
clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
printf("accept: %d\n", clientfd);
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) maxfd = clientfd;
if (-- nready == 0) continue;
}
int i = 0;
for (i = 0;i <= maxfd; i ++)
{
if (fds[i].revents & POLLIN)
{
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(i, buffer, BUFFER_LENGTH, 0);
if (ret == 0)
{
fds[i].fd = -1; // 需要将fd置为无效才行。
fds[i].events = 0;
close(i);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(i, buffer, ret, 0);
}
}
}
// getchar();
}
代码说明
结构中的events是我们传入到内核中的可读的项,而revents是从内核中反馈出来的。
struct poolfd
{
int fd;
short events;
short revent;
}
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。epoll可以同时处理大量的文件描述符,是基于事件驱动的IO操作方式,可以取代select和poll函数。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。另外,epoll使用红黑树存储管理事件,每次插入和删除事件的效率都是O(logn)的,其中n是红黑树中节点的个数。
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <sys/epoll.h>
#define BUFFER_LENGTH 1024
int main()
{
//open
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // io
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(struct sockaddr_in)); // 192.168.2.123
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
servaddr.sin_port = htons(9999);
if (-1 == bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)))
{
printf("bind failed: %s", strerror(errno));
return -1;
}
listen(sockfd, 10);
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int epfd = epoll_create(1);//1000 //list
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); //
struct epoll_event events[1024] = {0};
while (1)
{ // mainloop
int nready = epoll_wait(epfd, events, 1024, -1); //-1, 0,
if (nready < 0) continue;
int i = 0;
for (i = 0;i < nready;i ++) {
int connfd = events[i].data.fd;
if (sockfd == connfd)
{ // accept
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if (clientfd <= 0)
{
continue;
}
printf(" clientfd: %d\n", clientfd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
}
else if (events[i].events & EPOLLIN)
{
char buffer[10] = {0};
short len = 0;
recv(connfd, &len, 2, 0);
len = ntohs(len);
int n = recv(connfd, buffer, 10, 0);
if (n > 0)
{
printf("recv : %s\n", buffer);
send(connfd, buffer, n, 0);
}
else if (n == 0)
{
printf("close\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
close(connfd);
}
}
}
}
// getchar();
}
运行结果补充: