0、运行环境
# linux
# gcc
1、socket简介
Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求。
服务端步骤:
• socket:创建服务器socket实例
• bind:绑定ip地址和端⼝
• listen:开始监听
• accept:接收客户端请求
• read:读取客户端传来的数据
• write:给客户端传数据
• close:关闭socket,结束通信
客户端步骤:
• socket:创建客户端socket实例
• connect:连接服务器
• read:读取客户端传来的数据
• write:给客户端传数据
• close:关闭socket,结束通信
2、简易的socket通信程序
该程序只能实现多个个客户端连接,但是只能发送一条消息。后面版本逐渐解决问题
仅列出服务端程序,客户端用网络调试助手进行模拟调试。
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#define MAXLNE 4096
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
return 0;
}
3、多线程socket通信程序
上面的程序每个客户端只能发送一个消息是因为,accpet函数阻塞了。为了解决该问题考虑为每个客户端分配一个线程,来处理每个客户端的请求。
即将下列过程放入线程中完成:
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
改写如下:
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, &connfd);
并定义客户端的线程函数:
void* client_routine(void* arg){
int connfd = *(int *)arg;
char buff[MAXLNE];
while(1){
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
break;
}
}
}
该部分的所有代码如下:(编译时加上参数-lpthread)
例如文件为socket.c,则用gcc socket.c -o socket -lpthread
编译
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#define MAXLNE 4096
void* client_routine(void* arg){
int connfd = *(int *)arg;
char buff[MAXLNE];
while(1){
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
break;
}
}
}
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request-muti-clients========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, &connfd);
//close(connfd);
}
return 0;
}
缺点:上述采取的一客户端一线程的作法,内存消耗大,不适合大量客户端连接的情况,很难突破C10K
的并发量
3、基于select、poll、epoll的socket通信程序
相对于上面的做法,这三种方法是将所有要处理的套接字放在一个类似于容器(不同方法不同)的地方,然后不断的判断这里面的套接字的状态,如果这里面的套接字有读或写的请求,则会依次拿出来处理。关于select、poll、epoll的相关api以及完整的所有代码如下。
select
/*
// sizeof(fd_set) = 128bytes = 1024bit
// 也就是说最多可以检测1024个文件描述符
// fd_set是传入传出参数(指针)
#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);
- nfds : 委托内核检测的最大文件描述符+1
- readfds : 要检测的读文件描述符集合(就是委托内核检测读缓冲区中是否有数据进来)
- writefds : 要检测的写文件描述符集合(委托内核检测写缓冲区是否能写,没满就可以写)
- exceptfds : 没啥用
- timeout : 这个数据类型我们之前已经接触过了(一个结构体,包含s和ms两部分),用于设置超时时间
- 返回值 : -1表示失败, >0(n), 检测的集合中有n个描述符发生了变化
// 下面这些函数用于对fd_set进行各种操作
// 将参数fd指定的文件描述符置0
void FD_CLR(int fd, fd_set* set);
// 判断fd对应的文件描述符标志位是0还是1,返回对应的0和1
void FD_ISSET(int fd, fd_set* set);
// 将参数fd指定的文件描述符置1
void FD_SET(int fd, fd_set* set);
// 全部初始化为0
void FD_ZERO(fd_set* set);
*/
poll
/*
struct pollfd {
int fd; // 委托内核检测的文件描述符
short events; // 委托内核检测文件描述符的什么事件, POLLIN 数据(包括普通数据和优先数据)可读,POLLRDNORM 普通数据可读
short revents // 文件描述符实际发生的事件
}
int poll(struct pollfd* fds, nfds_t nfds, int timeout)
- fds : 需要检测的文件描述符集合
- nfds : 这是第一个参数数组中最后一个有效元素的下标+1
- timeout : 阻塞时长,0表示不阻塞,-1表示阻塞,当检测到需要检测的文件描述符发生变化时,接触阻塞,>0(n)阻塞时长n,单位为
返回值:
-1表示失败
>0(n)表示成功,表示检测到了n个文件描述符发生了变化
*/
epoll
/**
epoll_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 events
epoll_data_t data; // User data variable
};
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll_ctl参数:
epfd:指定的epoll模型。
op:表示具体的动作,用三个宏来表示。
EPOLL_CTL_ADD:注册新的文件描述符到指定的epoll模型中(往红黑树中新增节点)。
EPOLL_CTL_MOD:修改已经注册的文件描述符的监听事件(修改红黑树中特定节点的数据)。
EPOLL_CTL_DEL:从epoll模型中删除指定的文件描述符(删除红黑树中的节点)。
fd:需要监视的文件描述符。
event:需要监视该文件描述符上的哪些事件。
返回值:
函数调用成功返回0,调用失败返回-1,同时错误码会被设置。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_wait参数:
epfd:指定的epoll模型。
events:内核会将已经就绪的事件拷贝到events数组当中(events不能是空指针,内核只负责将就绪事件拷贝到该数组中,不会帮我们在用户态中分配内存)。
maxevents:events数组中的元素个数,该值不能大于创建epoll模型时传入的size值。
timeout:表示epoll_wait函数的超时时间,单位是毫秒(ms)。
返回值:
如果函数调用成功,则返回有事件就绪的文件描述符个数。
如果timeout时间耗尽,则返回0。
如果函数调用失败,则返回-1,同时错误码会被设置。
*/
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#define MAXLNE 4096
#define POLL_SIZE 1024
void* client_routine(void* arg){
int connfd = *(int *)arg;
char buff[MAXLNE];
while(1){
int n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
break;
}
}
}
int main(int argc, char **argv)
{
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
#if 0 // 只能一个客户端连接,并发送接收数据
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
return 0;
}
#elif 0 // 能够解决多个客户端连接,但是每个客户端只能发送一条数据,因为accept阻塞了
printf("========waiting for client's request========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
return 0;
}
#elif 0 // 创建线程,每个线程单独处理每个客户端的请求
/*
优点:逻辑简单
缺点:不适合大量客户端,很难突破一个数值 C10K
*/
printf("========waiting for client's request-muti-clients========\n");
while (1) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
pthread_t threadid;
pthread_create(&threadid, NULL, client_routine, &connfd);
//close(connfd);
}
return 0;
}
#elif 0 // select
/*
1、1个select,可以管理1024fd,多做几个select,能够突破C10K
2、无法突破C1000K
*/
/*每次调用select,都需要fd_set从用户态拷贝到内核态,然后再从内核态拷贝到用户态而且还需要遍历fd_set而且只支持1024个文件描述符*/
fd_set rdfs, rset; // 一个bit位数组,只置0-1。用来存放描述符的集合,如果有该描述符,则对应位置1。
FD_ZERO(&rdfs);
FD_SET(listenfd, &rdfs); // 将下标为listenfd的地方置1,代表将listenfd放入读描述符集
int max_fd = listenfd; // 为了配合select中第一个参数,最大的文件描述符 +1,代表之后select检查的时候只会检查到这。
while (1){
rset = rdfs;
// select调用返回时,除了那些已经就绪的描述符外,select将清除readfds、writefds和exceptfds中的所有没有就绪的描述符。
// 下述代码的rset集合中的未就绪的描述符就会被清除,也就是置0
// 返回值是就绪的描述符个数
int nready = select(max_fd + 1, &rset, NULL, NULL, NULL);
if(FD_ISSET(listenfd, &rset)){ // 判断监听的描述符是否在,即是否有新的客户端进行,有的话则接收并分配套接字
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
FD_SET(connfd, &rdfs); // 将新进来的客户端描述符放入集合
if(connfd > max_fd) max_fd = connfd;
if(nready-- == 0) continue; // 处理监听的描述符就绪外,其他的描述符都没有就绪
}
// 对就绪的客户端进行操作
for(int i = listenfd + 1; i <= max_fd; i++){
if(FD_ISSET(i, &rset)){
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
} else if (n == 0) {
// 客户端关闭连接,应该从集合中清除出去
FD_CLR(i, &rdfs);
close(i);
}
if(nready-- == 0) break; // 没有就绪的客户端了
}
}
}
return 0;
}
#elif 0 // poll
struct pollfd fds[POLL_SIZE] = {0};
fds[0].fd = listenfd;
fds[0].events = POLLIN;
int max_fd = listenfd;
while(1){
int nready = poll(fds, max_fd + 1, -1);
if(fds[0].revents & POLLIN){ // 同样先判断监听是否就绪
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
fds[connfd].fd = connfd; // 将新进来的客户端描述符放入fds
fds[connfd].events = POLLIN;
if(connfd > max_fd) max_fd = connfd;
if(nready-- == 0) continue; // 处理监听的描述符就绪外,其他的描述符都没有就绪
}
// 对就绪的客户端进行操作
for(int i = listenfd + 1; i <= max_fd; i++){
if(fds[i].revents & POLLIN){
n = recv(i, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(i, buff, n, 0);
} else if (n == 0) {
// 客户端关闭连接,应该从fds中清除出去
fds[i].fd = -1;
close(i);
}
if(nready-- == 0) break; // 没有就绪的客户端了
}
}
}
return 0;
}
#else // epoll
//poll/select -->
/*
* 相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
* 因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
*/
int epfd = epoll_create(1); // epoll_create函数用于创建一个epoll模型 :
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); // epoll_ctl函数用于向指定的epoll模型中注册事件,这里将监听描述符放入
while(1){
int nready = epoll_wait(epfd, events, POLL_SIZE, 5); // epoll_wait函数用于收集监视的事件中已经就绪的事件
if(nready == -1) continue;
for(int i = 0; i < nready; i++){
int clientfd = events[i].data.fd;
if(clientfd == listenfd){
struct sockaddr_in client;
socklen_t len = sizeof(client);
// accept分配fd是从小到大的分配,所有listenfd比后面进来客户端的值都小
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); // epoll_ctl函数用于向指定的epoll模型中注册事件:
}else if(events[i].events & EPOLLIN){
// printf("ac \n");
n = recv(clientfd, buff, MAXLNE, 0); // n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0) { // if (n > 0) {
buff[n] = '\0'; // buff[n] = '\0';
printf("recv msg from client: %s\n", buff); // printf("recv msg from client: %s\n", buff);
send(clientfd, buff, n, 0); // send(clientfd, buff, n, 0);
} else if (n == 0) {
// 客户端关闭连接,应该清除出去
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
return 0;
}
#endif