本博客已弃用,当时存在一些小细节错误后期也不再修改了
欢迎来我的新博客
先复习一下UDP协议:UDP协议为"user datagram protocol",用户数据报协议,对于UDP来说,其最小传输单元是数据报,并且是只管发送,不用建立连接,尽最大可能的发送数据,不保证可靠性。由于其不能拆分、合并应用层传递下来的报文,导致不能灵活的控制接收的次数与数量,且单个数据报最大只能是64k,但是由于不用建立连接,使其非常轻便、快速,且占用资源少。
TCP协议为"Transmission Control Protocol",传输控制协议。名副其实,TCP协议对数据传输的每一个细节几乎都控制到了,当然,这也导致TCP协议相当复杂,很多细节,如最开始的连接过程的三次握手到断开连接的四次挥手、超时重传机制、滑动窗口、流量控制、拥塞控制、延迟应答等等,都是TCP的非常重要的机制,在此我们就不讨论这些了,讲这方面的书籍与资源实在太多了。
因为既要保证可靠性,又要使性能尽可能的高,所以TCP的相对UDP较复杂是必然的。与UDP相反,TCP面向字节流,收发信息用系统调用write与read即可完成,既然面向字节流,那么无疑是可以自己灵活的控制读写次数,对于对方发来的100字节的数据,我可以分100次读,也可以一次读,同时在write数据时,如果数据太长,TCP协议会给你做好分包工作。当然,其传输的可靠性、重传机制以及高效性都非常不错,不过本篇我们的重点不是讲通信细节,而是服务器的实现,在此就不一一去说了。但是值得注意的是,这些理论是相当重要的,这是一个程序员必须保证的基本功,对于TCP协议应该清楚的知道每个api对应了哪些通信细节与机制。
TCP协议的常用接口
// 创建 socket ⽂文件描述符 ( 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端⼝口号 ( 服务器)
int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// 开始监听socket ( 服务器)
int listen(int socket, int backlog);
// 接收请求 (服务器)
int accept(int socket, struct sockaddr* address, socklen_t* address_len);
// 建⽴立连接 (客户端)
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen)
我们仍然写一个echo聊天服务器,首先是最简单的单进程阻塞版本
#include
#include
#include
#include
#include
#include
#include
//tcp_server ip port
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s is ip port",argv[0]);
return 1;
}
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0)
{
perror("listen_socket");
return 2;
}
printf("listen_socket:%d\n",listen_sock);
//填充本地信息
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号
local.sin_addr.s_addr = inet_addr(argv[1]);//IP地址
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(listen_sock,5) < 0)
{
perror("listen");
return 4;
}
for(;;)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len);
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
fflush(stdout);
char buf[128];
while(1)
{
ssize_t s = read(new_sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client: %s\n",buf);
write(new_sock,buf,strlen(buf));
}
else if(s == 0)//连接关闭
{
printf("quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(new_sock);
}
}
这样的单进程阻塞版本只能一个客户端和服务器通信,另外一个客户端想连接服务器是连不上来的。首先accept函数是阻塞的,如果没有客户端连接监听套接字那么accept会一直阻塞,知道有一个客户端连接到服务器,这时accept返回,代码走到收发数据的循环,知道第一个客户端断开,跳出收发循环前,服务器不会再走到accpet函数了,这就导致第二个客户端在第一个客户端退出前是无法连进来的,这显然是不合理的。TCP协议有面向连接的特点,因此这个问题是一定无法避免的。有三种常见的方案。在此先讲两种,第三种下次专门讲。
通过fork子进程,让子进程去进行真正的数据收发工作,而父进程则不断在接收各个客户端的连接。这里要处理的一个问题就是子进程死亡后如何处理,首先简单在父进程值wait()是绝对不行的,因为wait是阻塞的,这样又得等到子进程死亡后父进程才能继续让其他客户端接入。
最常用的方法就是在服务器中注册信号SIGCHLD为SIG_IGN,这是高并发多进程服务器的常用方法,在linux下,忽略SIGCHLD信号后,内核会子进程死亡后的僵尸进程交给init进程处理,除此之外还有一个理论上来说应用范围更广的方法:
pid_t id = fork();
if(id == 0) //child
{
if(fork() > 0)
{
exit(0);
}
close(listen_sock);
serviceIO(new_sock);
exit(0);
}
else //father
{
//close(new_sock);
waitpid(id,NULL,0); //绝对不会阻塞了,因为子进程立即退出了,父进程立马可以回收它
}在子进程中创建孙进程,在子进程让子进程自己立即退出,孙进程作为服务进程来收发数据,收发结束后退出。在此过程中,子进程立马就退出了,在父进程的执行流中waitpid马上就可以回收子进程并返回,绝对不会阻塞,而孙进程由于子进程的退出,自身成了孤儿进程,交由init进程管理其死亡后的处理。这个方法需要注意的是,一定要serviceIO函数中关闭用于数据传输的套接字,否则父进程的可用文件描述符会越来越少,会严重影响其性能瓶颈。
#include
#include
#include
#include
#include
#include
#include
#include
//tcp_server ip port
//多进程
void serviceIO(int sock)
{
char buf[128];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client: %s\n",buf);
write(sock,buf,strlen(buf));
}
else if(s == 0)//连接关闭
{
printf("quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(sock);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s is ip port",argv[0]);
return 1;
}
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0)
{
perror("listen_socket");
return 2;
}
printf("listen_socket:%d\n",listen_sock);
//填充本地信息
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号
local.sin_addr.s_addr = inet_addr(argv[1]);//IP地址
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(listen_sock,5) < 0)
{
perror("listen");
return 4;
}
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len); //阻塞的
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pid_t id = fork();
if(id == 0) //child
{
if(fork() > 0)
{
exit(0);
}
close(listen_sock);
serviceIO(new_sock);
exit(0);
}
else //father
{
//close(new_sock);
waitpid(id,NULL,0); //绝对不会阻塞了,因为子进程立即退出了,父进程立马可以回收它
}
}
}
多线程版本通过创建线程,使各个线程与客户端进程数据传输,而主线程则不断accept各个客户端的连接,值得注意的是线程要设为detach状态,是其死亡后自会释放资源,在线程的处理函数中也一定要关闭用于数据传输的套接字,否则也会影响主线程。
#include
#include
#include
#include
#include
#include
#include
#include
#include
//多线程
//tcp_server ip port
void serviceIO(int sock)
{
char buf[128];
while(1)
{
ssize_t s = read(sock,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("client: %s\n",buf);
write(sock,buf,strlen(buf));
}
else if(s == 0)//连接关闭
{
printf("quit!\n");
break;
}
else
{
perror("read");
break;
}
}
close(sock);
}
void* service(void* arg)
{
int sock = (int)arg;
serviceIO(sock);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s is ip port",argv[0]);
return 1;
}
int listen_sock = socket(AF_INET,SOCK_STREAM,0);
if(listen_sock < 0)
{
perror("listen_socket");
return 2;
}
printf("listen_socket:%d\n",listen_sock);
//填充本地信息
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(atoi(argv[2]));//端口号
local.sin_addr.s_addr = htonl(INADDR_ANY);//IP地址
if(bind(listen_sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
return 3;
}
if(listen(listen_sock,5) < 0)
{
perror("listen");
return 4;
}
while(1)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len); //阻塞的
if(new_sock < 0)
{
perror("accept");
continue;
}
printf("get new link![%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
pthread_t id;
pthread_create(&id,NULL,service,(void *)new_sock);
pthread_detach(id);
}
}
#include
#include
#include
#include
#include
#include
#include
//tcp_client ip port
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s is server_ip server_port",argv[0]);
return 1;
}
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
return 2;
}
printf("socket:%d\n",sock);
//填充本地信息
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(atoi(argv[2]));//端口号
server.sin_addr.s_addr = inet_addr(argv[1]);//IP地址
if(connect(sock, (struct sockaddr*)&server,sizeof(server)) < 0)
{
perror("connect");
return 3;
}
char buf[128];
while(1)
{
printf("Please Enter:");
fflush(stdout);
ssize_t s = read(0,buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
write(sock,buf,strlen(buf));
read(sock,buf,sizeof(buf)-1);
printf("server echo: %s\n",buf);
}
}
}
源码