linux的网络编程是基与socket编程的;首先总结socket编程;基于socket编程实现TCP通信的多并发的实现方式:多进程,多线程,select,poll,epoll,;这几种方式的如何实现,异同优缺点;最后介绍一下libevent框架;在实现网络编程的过程中,以上这些方式是基础的方法,在实现网络编程一般采用的是epoll+多线程;这是最高效的一种方式;
七层面试需要,4层做项目需要
int socket(int domain, int type, int protocol);
socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。
protofamily:
即协议域,又称为协议族(family)。常用的协议族有,AF_INET(IPV4)、AF_INET6(IPV6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。
协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
type:
指定socket类型。常用的socket类型有,SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等等
protocol:
就是指定协议。常用的协议有,IPPROTO_TCP、IPPTOTO_UDP、IPPROTO_SCTP、IPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议
0是默认前面的协议;
注意:
当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。
2.int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。
sockfd:
即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
addr:
一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
addrlen:对应的是地址的长度。
通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。
注意:
在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,
3.int listen(int sockfd, int backlog);
第一个参数即为要监听的socket描述字,
第二个参数为相应socket可以排队的最大连接个数。
socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。
调用了listen方法后,服务端就打开了三次握手的开关,能够处理来自客户端的SYN分节了,只要三次握手完成,客户端就会connect成功,而跟服务端调用accept没任何关系,accept只是去取已完成连接队列的对头项。(引自:https://blog.csdn.net/junjun150013652/article/details/37966901?utm_source=tuicool&utm_medium=referral)
4.int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); //返回连接connect_fd
accept默认会阻塞进程,直到有一个客户连接建立后返回,它返回的是一个新可用的套接字,
这个套接字是连接套接字。
TCP服务器端依次调用socket()、bind()、listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()、connect()之后就向TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,如果accept成功返回,则服务器与客户已经正确建立连接了,此时服务器通过accept返回的套接字来完成与客户的通信。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。
参数 sockfd
参数sockfd就是上面解释中的监听套接字,这个套接字用来监听一个端口,当有一个客户与服务器连接时,它使用这个一个端口号,而此时这个端口号正与这个套接字关联。当然客户不知道套接字这些细节,它只知道一个地址和一个端口号。
参数 addr
这是一个结果参数,它用来接受一个返回值,这返回值指定客户端的地址,当然这个地址是通过某个地址结构来描述的,用户应该知道这一个什么样的地址结构。如果对客户的地址不感兴趣,那么可以把这个值设置为NULL
参数 len
如同大家所认为的,它也是结果的参数,用来接受上述addr的结构的大小的,它指明addr结构所占有的字节个数。同样的,它也可以被设置为NULL。
5.int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
第一个参数即为客户端的socket描述字
第二参数为服务器的socket地址
第三个参数为socket地址的长度
客户端通过调用connect函数来建立与TCP服务器的连接。
监听套接字: 监听套接字正如accept的参数sockfd,它是监听套接字,在调用listen函数之后,是服务器开始调用socket()函数生成的,称为监听socket描述字(监听套接字)
连接套接字:一个套接字会从主动连接的套接字变身为一个监听套接字;而accept函数返回的是已连接socket描述字(一个连接套接字),它代表着一个网络已经存在的点点连接。
一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。
为什么要有两种套接字?原因很简单,如果使用一个描述字的话,那么它的功能太多,使得使用很不直观,同时在内核确实产生了一个这样的新的描述字。
连接套接字socketfd_new 并没有占用新的端口与客户端通信,依然使用的是与监听套接字socketfd一样的端口号
多进程:
父进程的主要作用是创建监听文件描述符;
子进程主要用来通信;
每启动一个客户端,就创建一个进程;
原因:accept()函数在没有客户端发请求的时候,处于阻塞状态,所以下面的fork()不运行;
fork()每次都会拷贝一份一模一样的内存,里面包含了通信文件描述符cfd;在通信的时候使用;这种机制会消耗很大的资源,每个客户端都需要拷贝一份新的内存,不推荐使用 ;
#include
#include
#include
#include
#include
#include
#include
#include
// 信号捕捉函数
void recycleChild(int num)
{
// 资源回收
while(1)
{
int ret = waitpid(-1, NULL, WNOHANG);
if(ret == -1)
{
printf("所有的子进程回收完毕!!!\n");
break;
}
else if(ret == 0)
{
printf("剩下的子进程都还活着!!!\n");
break;
}
else
{
printf("child die, pid = %d\n", ret);
}
}
}
int main()
{
// 1. 创建用于监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET; // ipv4
addr.sin_port = htons(8989); // 字节序应该是网络字节序
//inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
addr.sin_addr.s_addr = INADDR_ANY; // == 0, 获取IP的操作交给了内核
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3.设置监听
ret = listen(fd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 注册新号捕捉
struct sigaction act;
act.sa_flags = 0;
act.sa_handler = recycleChild;
sigemptyset(&act.sa_mask);
sigaction(SIGCHLD, &act, NULL);
while(1)
{
// 4. 等待, 接受连接请求
struct sockaddr_in addrCli;
int len = sizeof(addrCli);
printf("正在焦急等待客户端的连接...\n");
int connfd = accept(fd, (struct sockaddr*)&addrCli, &len);
if(connfd == -1)
{
if(errno == EINTR)
{
continue;
}
perror("accept");
exit(0);
}
// 成功和客户端建立了连接
// 创建子进程
pid_t pid = fork();
if(pid == 0)
{
int num = 0;
while(1)
{
// 读数据
char recvBuf[1024];
// 如果客户端没有发送数据, 默认阻塞
int ret = read(connfd, recvBuf, sizeof(recvBuf));
if(ret == -1)
{
perror("read");
break;
}
else if(ret == 0)
{
printf("客户端已经断开了连接...\n");
break;
}
else
{
// 打印客户端地址信息
char ip[32];
inet_ntop(AF_INET, &addrCli.sin_addr.s_addr, ip, sizeof(ip));
printf("client IP: %s, Port: %d\n", ip, ntohs(addrCli.sin_port));
printf("客户端说: %s\n", recvBuf);
// 写数据
sprintf(recvBuf, "你好, 客户端 - %d\n", num++);
write(connfd, recvBuf, strlen(recvBuf)+1);
}
}
close(connfd); // 通信
// 退出当前子进程
exit(0);
}
}
// 释放资源
close(fd); // 监听
return 0;
}
多线程:
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct SockInfo
{
int fd; // 通信
pthread_t tid; // 线程ID
struct sockaddr_in addr; // 地址信息
};
struct SockInfo infos[128];
void* working(void* arg)
{
while(1)
{
struct SockInfo* info = (struct SockInfo*)arg;
// 接收数据
char buf[1024];
int ret = read(info->fd, buf, sizeof(buf));
if(ret == 0)
{
printf("客户端已经关闭连接...\n");
info->fd = -1;
break;
}
else if(ret == -1)
{
printf("接收数据失败...\n");
info->fd = -1;
break;
}
else
{
write(info->fd, buf, strlen(buf)+1);
}
}
return NULL;
}
int main()
{
// 1. 创建用于监听的套接字
int fd = socket(AF_INET, SOCK_STREAM, 0);
if(fd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET; // ipv4
addr.sin_port = htons(8989); // 字节序应该是网络字节序
//inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
addr.sin_addr.s_addr = INADDR_ANY; // == 0, 获取IP的操作交给了内核
int ret = bind(fd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3.设置监听
ret = listen(fd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待, 接受连接请求
int len = sizeof(struct sockaddr);
// 数据初始化
int max = sizeof(infos) / sizeof(infos[0]);
for(int i=0; i<max; ++i)
{
bzero(&infos[i], sizeof(infos[i]));
infos[i].fd = -1;
infos[i].tid = -1;
}
printf("111111111111111\n");
// 父进程监听, 子进程通信
while(1)
{
// 创建子线程
struct SockInfo* pinfo;
for(int i=0; i<max; ++i)
{
printf("fd = %d\n", infos[i].fd);
if(infos[i].fd == -1)
{
pinfo = &infos[i];
printf("i = %d\n", i);
break;
}
if(i == max-1)
{
sleep(1);
i--;
}
}
printf("xxxxxxx\n");
int connfd = accept(fd, (struct sockaddr*)&pinfo->addr, &len);
printf("parent thread, connfd: %d\n", connfd);
if(connfd == -1)
{
perror("accept");
exit(0);
}
pinfo->fd = connfd;
pthread_create(&pinfo->tid, NULL, working, pinfo);
pthread_detach(pinfo->tid);
}
// 释放资源
close(fd); // 监听
return 0;
}
#include
#include
#include
#include
#include
#include
int main()
{
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 监听
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待连接 -> 循环
// 检测 -> 读缓冲区, 委托内核去处理
// 数据初始化, 创建自定义的文件描述符集
fd_set rdset, tmp;
FD_ZERO(&rdset);
FD_SET(lfd, &rdset);
int maxfd = lfd;
while(1)
{
// 委托内核检测
tmp = rdset;
ret = select(maxfd+1, &tmp, NULL, NULL, NULL);
if(ret == -1)
{
perror("select");
exit(0);
}
// 检测的度缓冲区有变化
// 有新连接
if(FD_ISSET(lfd, &tmp))
{
// 接收连接请求
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 这个accept是不会阻塞的
int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
// 委托内核检测connfd的读缓冲区
FD_SET(connfd, &rdset);
maxfd = connfd > maxfd ? connfd : maxfd;
}
// 通信, 有客户端发送数据过来
for(int i=lfd+1; i<=maxfd; ++i)
{
// 如果在集合中, 说明读缓冲区有数据
if(FD_ISSET(i, &tmp))
{
char buf[128];
int ret = read(i, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
exit(0);
}
else if(ret == 0)
{
printf("对方已经关闭了连接...\n");
FD_CLR(i, &rdset);
close(i);
}
else
{
printf("客户端say: %s\n", buf);
write(i, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 监听
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 4. 等待连接 -> 循环
// 检测 -> 读缓冲区, 委托内核去处理
// 数据初始化, 创建自定义的文件描述符集
struct pollfd fds[1024];
// 初始化
for(int i=0; i<1024; ++i)
{
fds[i].fd = -1;
fds[i].events = POLLIN;
}
fds[0].fd = lfd;
int maxfd = 0;
while(1)
{
// 委托内核检测
ret = poll(fds, maxfd+1, -1);
if(ret == -1)
{
perror("select");
exit(0);
}
// 检测的度缓冲区有变化
// 有新连接
if(fds[0].revents & POLLIN)
{
// 接收连接请求
struct sockaddr_in sockcli;
int len = sizeof(sockcli);
// 这个accept是不会阻塞的
int connfd = accept(lfd, (struct sockaddr*)&sockcli, &len);
// 委托内核检测connfd的读缓冲区
int i;
for(i=0; i<1024; ++i)
{
if(fds[i].fd == -1)
{
fds[i].fd = connfd;
break;
}
}
maxfd = i > maxfd ? i : maxfd;
}
// 通信, 有客户端发送数据过来
for(int i=1; i<=maxfd; ++i)
{
// 如果在集合中, 说明读缓冲区有数据
if(fds[i].revents & POLLIN)
{
char buf[128];
int ret = read(fds[i].fd, buf, sizeof(buf));
if(ret == -1)
{
perror("read");
exit(0);
}
else if(ret == 0)
{
printf("对方已经关闭了连接...\n");
close(fds[i].fd);
fds[i].fd = -1;
}
else
{
printf("客户端say: %s\n", buf);
write(fds[i].fd, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
int main()
{
// 1.创建套接字
int lfd = socket(AF_INET, SOCK_STREAM, 0);
if(lfd == -1)
{
perror("socket");
exit(0);
}
// 2. 绑定 ip, port
struct sockaddr_in addr;
addr.sin_port = htons(10000);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = INADDR_ANY;
int ret = bind(lfd, (struct sockaddr*)&addr, sizeof(addr));
if(ret == -1)
{
perror("bind");
exit(0);
}
// 3. 监听
ret = listen(lfd, 100);
if(ret == -1)
{
perror("listen");
exit(0);
}
// 创建epoll树
int epfd = epoll_create(1000);
if(epfd == -1)
{
perror("epoll_create");
exit(0);
}
// 将监听lfd添加到树上
struct epoll_event ev;
// 检测事件的初始化
ev.events = EPOLLIN ;
ev.data.fd = lfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
struct epoll_event events[1024];
// 开始检测
while(1)
{
int nums = epoll_wait(epfd, events, sizeof(events)/sizeof(events[0]), -1);
printf("numbers = %d\n", nums);
// 遍历状态变化的文件描述符集合
for(int i=0; i<nums; ++i)
{
int curfd = events[i].data.fd;
// 有新连接
if(curfd == lfd)
{
struct sockaddr_in clisock;
int len = sizeof(clisock);
int connfd = accept(lfd, (struct sockaddr*)&clisock, &len);
if(connfd == -1)
{
perror("accept");
exit(0);
}
// 将通信的fd挂到树上
//ev.events = EPOLLIN | EPOLLOUT;
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
// 通信
else
{
// 读事件触发, 写事件触发
if(events[i].events & EPOLLOUT)
{
continue;
}
char buf[128];
int count = read(curfd, buf, sizeof(buf));
if(count == 0)
{
printf("client disconnect ...\n");
close(curfd);
// 从树上删除该节点
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
}
else if(count == -1)
{
perror("read");
exit(0);
}
else
{
// 正常情况
printf("client say: %s\n", buf);
write(curfd, buf, strlen(buf)+1);
}
}
}
}
close(lfd);
return 0;
}
sever:
#include
#include
#include
#include
#include
int main()
{
// 1. 创建事件处理框架
struct event_base* base = event_base_new();
// 打印支持的IO转接函数
const char** method = event_get_supported_methods();
for(int i=0; method[i] != NULL; ++i)
{
printf("%s\n", method[i]);
}
printf("current method: %s\n", event_base_get_method(base));
// 创建子进程
pid_t pid = fork();
if(pid == 0)
{
// 子进程中event_base也会被复制,在使用这个base时候要重新初始化
event_reinit(base);
}
// 2. 释放资源
event_base_free(base);
return 0;
}
client:
#include
#include
#include
#include
#include
#include
#include
// read缓冲区的回调
void read_cb(struct bufferevent* bev, void* arg)
{
printf("arg value: %s\n", (char*)arg);
// 读缓冲区的数据
char buf[128];
int len = bufferevent_read(bev, buf, sizeof(buf));
printf("read data: len = %d, str = %s\n", len, buf);
// 回复数据
bufferevent_write(bev, buf, len);
printf("数据发送完毕...\n");
}
// 写缓冲区的回调
// 调用的时机: 写缓冲区中的数据被发送出去之后, 该函数被调用
void write_cb(struct bufferevent* bev, void* arg)
{
printf("arg value: %s\n", (char*)arg);
printf("数据已经发送完毕...xxxxxxxxxxxx\n");
}
// 事件回调
void events_cb(struct bufferevent* bev, short event, void* arg)
{
if(event & BEV_EVENT_ERROR)
{
printf("some error happened ...\n");
}
else if(event & BEV_EVENT_EOF)
{
printf("server disconnect ...\n");
}
// 终止连接
bufferevent_free(bev);
}
void send_msg(evutil_socket_t fd, short ev, void * arg)
{
// 将写入到终端的数据读出
char buf[128];
int len = read(fd, buf, sizeof(buf));
// 发送给服务器
struct bufferevent* bev = (struct bufferevent*)arg;
bufferevent_write(bev, buf, len);
}
int main()
{
struct event_base * base = event_base_new();
// 1. 创建通信的套接字
struct bufferevent* bufev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
// 2. 连接服务器
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(9898); // 服务器监听的端口
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);
// 这个函数调用成功, == 服务器已经成功连接
bufferevent_socket_connect(bufev, (struct sockaddr*)&addr, sizeof(addr));
// 3. 通信
// 给bufferevent的缓冲区设置回调
bufferevent_setcb(bufev, read_cb, write_cb, events_cb, (void*)"hello, world");
bufferevent_enable(bufev, EV_READ);
// 创建一个普通的输入事件
struct event* myev = event_new(base, STDIN_FILENO, EV_READ|EV_PERSIST, send_msg, bufev);
event_add(myev, NULL);
event_base_dispatch(base);
event_free(myev);
event_base_free(base);
return 0;
}