1. 单客户端单服务器TCP通信
服务端
include <sys/socket.h>
#include <error.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#define BUFFER_LENGTH 1024
int main(){
// 创建socket:int socket(int domain, int type, int protocol)
// domain参数告诉系统使用哪个底层协议族:
// 对于TCP/IP协议族而言,AF_INET用于IPv4,AF_INET6用于IPv6
// type参数指定服务类型。服务类型主要有SOCK_STREAM服务(流服务)和SOCK_UGRAM(数据报)服务
// protocol参数一般设置为0,表示使用默认协议
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
// sockaddr_in是TCP/IP协议族专用socket地址结构体,用于IPv4
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(2048);
// 命名socket:int bind(int sockfd, const struct sockaddr* my_addr, socklen_t addrlen)
// 在服务器程序中,我们要给socket命名这样客户端才能知道如何连接它,客户端则不需要命名socket
// bind将my_addr所指的socket地址分配给未命名的sockfd文件描述符
// addrlen参数指出该socket地址的长度
int name_fd = bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
if(name_fd == -1){
perror("bind error");
return -1;
}
// 监听socket:int listen(int sockfd, int backlog)
// backlog参数提示内核监听队列的最大长度,如果长度超过backlog,服务器将不受理新的客户连接
name_fd = listen(sockfd, 10);
// 接受连接:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
// 从listen监听队列中接受一个连接
// sockfd参数是执行过listen系统调用的监听socket
// addr参数用来获取被接受连接的远端socket地址,该socket地址的长度由addrlen参数指出
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
// TCP数据读写:
// ssize_t recv(int sockfd, void *buf, size_t len, int flags)
// recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flags参数通常设置为0
// ssize_t send(int sockfd, const void *buf, size_t len, int flags)
// send往sockfd上写入数据,buf和len参数分别指定写缓冲区的位置和大小
while(1){ // while循环可以多次接收客户端发送的数据
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
// 服务器解析收到的内容然后才能确定发送给谁
if(ret == 0){
close(clientfd);
break;
}
send(clientfd, buffer, ret, 0);
}
return 0;
}
客户端
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#define BUFFER_LENGTH 1024
int main(){
// 创建socket
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd == -1){
perror("socket error");
return -1;
}
// 设置服务器地址信息
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = inet_addr("服务器IP地址"); // 服务器的IP地址
serveraddr.sin_port = htons(2048); // 服务器监听的端口号
// 连接服务器:int connect(int sockfd, const struct sockaddr *serv_addr, socklen_t addrlen)
// serv_addr参数是服务器监听的socket地址,addrlen参数则指定这个地址的长度
// 一旦成功建立连接,sockfd就唯一地标识了这个连接,客户端就可以通过读写sockfd来与服务器通信
int connectfd = connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
if (connectfd == -1){
perror("connect error");
close(sockfd);
return -1;
}
// 发送数据给服务器
char sendData[BUFFER_LENGTH] = "Hello, Server!";
send(sockfd, sendData, strlen(sendData), 0);
// 接收服务器的响应
char recvData[BUFFER_LENGTH] = {0};
recv(sockfd, recvData, BUFFER_LENGTH, 0);
printf("Received from server: %s\n", recvData);
// 关闭连接
close(sockfd);
return 0;
}
2. 单服务端多客户端TCP通信
2.1 函数实现多线程
服务端
#include <pthread.h>
void *client_thread(void *arg){
int clientfd = *(int*)arg;
while(1){ // while循环可以多次接收客户端发送的数据
char buffer[BUFFER_LENGTH] = {0};
int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
if(ret == 0){
close(clientfd);
break;
}
send(clientfd, buffer, ret, 0);
}
}
int main(){
...
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
pthread_t threadid;
pthread_create(&threadid, NULL, client_thread, &clientfd);
return 0;
}
2.2 select 实现多线程
- 多线程客户端的做法是一个客户端启动一个线程,而 select( ) 把所有的连接都放在一个线程里
- select(maxfd, rset, wset, eset, timeout)
- maxfd:最大的fd(fd是依次增加的int值。其中0是stdin,1是stdout,2是stderror)
- rset:可读集合、wset:可写集合、eset:出错集合
- timeout:超时(多次时间轮询一次)
- select处理的最大int长度是1024,是在内核里确定的,如果io更大则会提示数组越界
int main(){
...
name_fd = listen(sockfd, 10);
fd_set rfds, rset;
FD_ZERO(&rfds); // 初始化文件描述符集 rfds,并将其中的所有文件描述符清零
FD_SET(sockfd, &rfds); // 将监听的 sockfd 添加到文件描述符集 rfds
int maxfd = sockfd;
int clientfd = 0;
while(1){
rset = rfds;
// 使用 select 函数监视多个文件描述符的可读性,它返回准备好读取的文件描述符数
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){
FD_CLR(i, &rfds);
close(i);
break;
}
send(i, buffer, ret, 0);
}
}
}
return 0;
}
- select 的缺点:
- 参数较多,每个参数需要单独去维护
- 每次需要遍历io集合,从而返回就绪集合
- 每次都要把待检测的io集合copy进内核,对性能有影响(应用层的多个io要一一对应内核中的bit_set数组)
- 对io的数量有限制(最多1024个io,在内核中设定的)
2.3 poll 实现多线程
- poll只是select在参数上的一个优化,减少了 一些不必要参数的复制,他们底层实现是一样,都是基于select设计的
#include <sys/poll.h>
#define POLL_SIZE 1024
int main(){
...
name_fd = listen(sockfd, 10);
struct pollfd fds[POLL_SIZE] = {0};
fds[sockfd].fd = sockfd;
fds[sockfd].events = POLLIN; // 事件类型为 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;
fds[i].events = 0;
close(i);
break;
}
printf("ret: %d, buffer: %s\n", ret, buffer);
send(i, buffer, ret, 0);
}
}
}
return 0;
}
2.4 epoll 实现多线程
#include <sys/poll.h>
#define POLL_SIZE 1024
int main(){
...
name_fd = listen(sockfd, 10);
int epfd = epoll_create(1); // 参数 1 是一个暂时无意义的值,大于0即可
struct epoll_event ev;
ev.events = EPOLLIN; // 设置要监听的事件类型,EPOLLIN,表示有数据可读
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 将监听套接字添加到 epoll 实例中进行监听
struct epoll_event events[1024] = {0}; // 创建一个用于存储触发事件的 epoll 事件数组
while(1){
int nready = epoll_wait(epfd, events, 1024, -1);
if(nready < 0) continue;
int i = 0;
for(i=0; i<nready; ++i){
int connfd = events[i].data.fd; // 获取触发事件的文件描述符
if(sockfd == connfd){
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
if(clientfd <=0) continue;
// 修改新客户端文件描述符的监听事件为EPOLLIN(有数据可读)和EPOLLET(设置边缘触发)
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};
int count = recv(connfd, buffer, 10, 0);
if(count == 0){
printf("close\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, NULL);
close(i);
continue;
}
send(connfd, buffer, count, 0);
}
}
}
}
- 水平触发LT:io传过来40个字节数据,服务端一次最多接收10个,可以一直接收直到io数据传完
- 边沿触发ET:服务端只接收一次10个字节的数据,每send一次接收一次
2.4 事件驱动 reactor 实现多线程
- select( )、poll( )、epoll( )都是针对io处理的,io越多,代码越冗杂
- reactor是针对事件 EPOLLIN / EPOLLOUT 处理的,即需要读一个事件如何做,需要写一个事件如何做
if(events & EPOLLIN){ ... }
else if(events & EPOLLOUT){ ... }
- reactor把整个网络io封装只留下两个接口,一个rbuffer,一个wbuffer。reactor只关注rbuffer是否有数据,并且把wbuffer中的数据发送即可,rbuffer针对EPOLLIN,wbuffer针对EPOLLOUT
- listenfd 触发 EPOLLIN 事件时,执行 accept_cb( )
- clientfd 触发 EPOLLIN 事件时,执行 recv_cb( )
- listenfd 触发 EPOLLOUT 事件时,执行 send_cb( )
#include <sys/socket.h>
#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>
#include <sys/time.h>
#define BUFFER_LENGTH 512
typedef int (*RCALLBACK)(int fd);
int accept_cb(int fd);
int recv_cb(int fd);
int send_cb(int fd);
struct conn_item{
int fd;
char rbuffer[BUFFER_LENGTH];
int rlen;
char wbuffer[BUFFER_LENGTH];
int wlen;
union{
RCALLBACK accept_callback;
RCALLBACK recv_callback;
} recv_t;
RCALLBACK send_callback;
};
int epfd = 0;
struct conn_item connlist[1048576] = {0};
int set_event(int fd, int event, int flag){
// flag = 1 add, flag = 0 mod
if(flag){
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
}
else{
struct epoll_event ev;
ev.events = event;
ev.data.fd = fd;
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
}
}
int accept_cb(int fd){
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(fd, (struct sockaddr*)&clientaddr, &len);
if (clientfd < 0) {
return -1;
}
set_event(clientfd, EPOLLIN, 1);
connlist[clientfd].fd = clientfd;
memset(connlist[clientfd].rbuffer, 0, BUFFER_LENGTH);
connlist[clientfd].rlen = 0;
memset(connlist[clientfd].wbuffer, 0, BUFFER_LENGTH);
connlist[clientfd].wlen = 0;
connlist[clientfd].recv_t.recv_callback = recv_cb;
connlist[clientfd].send_callback = send_cb;
return clientfd;
}
int recv_cb(int fd){
char *buffer = connlist[fd].rbuffer;
int idx = connlist[fd].rlen;
int count = recv(fd, buffer+idx, BUFFER_LENGTH-idx, 0);
if (count == 0) {
printf("disconnect\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
return -1;
}
connlist[fd].rlen += count;
memcpy(connlist[fd].wbuffer, connlist[fd].rbuffer, connlist[fd].rlen);
connlist[fd].wlen = connlist[fd].rlen;
connlist[fd].rlen -= connlist[fd].rlen;
set_event(fd, EPOLLOUT, 0);
return count;
}
int send_cb(int fd){
char *buffer = connlist[fd].wbuffer;
int idx = connlist[fd].wlen;
int count = send(fd, buffer, idx, 0);
set_event(fd, EPOLLIN, 0);
return count;
}
int init_server(unsigned short port){
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serveraddr;
memset(&serveraddr, 0, sizeof(struct sockaddr_in));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(port);
int name_fd = bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(struct sockaddr));
if (name_fd == -1){
perror("bind error");
return -1;
}
name_fd = listen(sockfd, 10);
return sockfd;
}
int main(){
int port_count = 20;
unsigned short port = 2048;
int i = 0;
int epfd = epoll_create(1);
for(i = 0; i < port_count; ++i){
int sockfd = init_server(port + i); // 2048, 2049, 2050, 2051 ... 2057
connlist[sockfd].fd = sockfd;
connlist[sockfd].recv_t.accept_callback = accept_cb;
set_event(sockfd, EPOLLIN, 1);
}
struct epoll_event events[1024] = {0};
while(1){
int nready = epoll_wait(epfd, events, 1024, -1);
int i = 0;
for (i = 0; i < nready; i ++) {
int connfd = events[i].data.fd;
if (events[i].events & EPOLLIN) {
int count = connlist[connfd].recv_t.recv_callback(connfd);
printf("recv count: %d <-- buffer: %s\n", count, connlist[connfd].rbuffer);
}
else if (events[i].events & EPOLLOUT) {
// printf("send --> buffer: %s\n", connlist[connfd].wbuffer);
int count = connlist[connfd].send_callback(connfd);
}
}
}
return 0;
}