各种类型Tcp服务器
我们在前面的网络编程套接字(二)中写出了一个单执行流的服务器
我们再来回顾下它的运行

我们首先启动服务器 之后启动客户端1 最后启动客户端2
我们发现启动客户端1之后向服务器发送数据服务器很快的就回显了一个数据并且打印了得到一个新连接
可是在客户端2连接的时候却没有发生任何情况

当我们的客户端1退出的时候 服务器接受到了客户端2的连接并且回显了数据
单执行流服务器
这是因为我们的服务器是单执行流的 所以在同一时间只能有一个客户端接受服务
当服务端调用accept函数获取到连接后就给该客户端提供服务 但在服务端提供服务期间可能会有其他客户端发起连接请求 但由于当前服务器是单执行流的 只能服务完当前客户端后才能继续服务下一个客户端
客户端为什么会显示连接成功
服务器是处于监听状态的 在我们的客户端2发送连接请求的时候实际上已经被监听到了 只不过服务端没有调用accept函数将该连接获取上来
实际在底层会为我们维护一个连接队列 服务端没有accept的新连接就会放到这个连接队列当中 而这个连接队列的最大长度就是通过listen函数的第二个参数来指定的 因此服务端虽然没有获取第二个客户端发来的连接请求 但是在第二个客户端那里显示是连接成功的
如何解决?
单执行流的服务器一次只能给一个客户端提供服务 此时服务器的资源并没有得到充分利用 因此服务器一般是不会写成单执行流的 要解决这个问题就需要将服务器改为多执行流的 此时就要引入多进程或多线程
我们将之前的单执行流服务器改为多进程服务器
当服务端调用accept函数获取到新连接后不是由当前执行流为该连接提供服务 而是当前执行流调用fork函数创建子进程 然后让子进程为父进程获取到的连接提供服务
由于父子进程是两个不同的执行流 当父进程调用fork创建出子进程后 父进程就可以继续从监听套接字当中获取新连接 而不用关心获取上来的连接是否服务完毕
子进程继承父进程的文件描述符表
需要注意的是 文件描述符表是隶属于一个进程的 子进程创建后会继承父进程的文件描述符表
比如父进程打开了一个文件 该文件对应的文件描述符是3 此时父进程创建的子进程的3号文件描述符也会指向这个打开的文件 而如果子进程再创建一个子进程 那么子进程创建的子进程的3号文件描述符也同样会指向这个打开的文件

但是当父进程创建出子进程之后 父子进程就会保持独立性了 此时父进程文件描述符表的变化不会影响子进程的文件描述符表
在我们之前学习的匿名管道通信时 我们就是使用的这个原理
父进程首先使用pipe函数得到两个文件描述符 一个是文件读端一个是文件的写端 此时父进程创建的子进程会继承这两个文件描述符
之后父子进程一个关闭管道的读端 另一个关闭管道的写端 这时父子进程文件描述符表的变化是不会相互影响的 此后父子进程就可以通过这个管道进行单向通信了
对于套接字文件也是一样的 父进程创建的子进程也会继承父进程的套接字文件 此时子进程就能够对特定的套接字文件进行读写操作 进而完成对对应客户端的服务
等待子进程问题
当父进程创建出子进程后 父进程是需要等待子进程退出的 否则子进程会变成僵尸进程 进而造成内存泄漏
因此服务端创建子进程后需要调用wait或waitpid函数对子进程进行等待
此时我们就有两种等待方式 阻塞式等待和非阻塞式等待:
总之 服务端要等待子进程退出 无论采用阻塞式等待还是非阻塞式等待 都不尽人意 此时我们可以考虑让服务端不等待子进程退出
不等待子进程退出的方式
让父进程不等待子进程退出 常见的方式有两种:
实际当子进程退出时会给父进程发送SIGCHLD信号 如果父进程将SIGCHLD信号进行捕捉 并将该信号的处理动作设置为忽略 此时父进程就只需专心处理自己的工作 不必关心子进程了
下面是我们的处理代码 其中比较核心的代码是这一行
signal(SIGCHLD, SIG_IGN);
class TcpServer
{
public:
void Start()
{
signal(SIGCHLD, SIG_IGN); //忽略SIGCHLD信号
for (;;){
//获取连接
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
std::cerr << "accept error, continue next" << std::endl;
continue;
}
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
pid_t id = fork();
if (id == 0){ //child
//处理请求
Service(sock, client_ip, client_port);
exit(0); //子进程提供完服务退出
}
}
}
private:
int _listen_sock; //监听套接字
int _port; //端口号
};
下面是在此运行的结果

我们可以发现 加上这几行代码之后我们就可以让服务器服务多个客户端了
我们也可以让服务端创建出来的子进程再次进行fork 让孙子进程为客户端提供服务 此时我们就不用等待孙子进程退出了
命名:
我们让爸爸进程创建完孙子进程后立刻退出 此时服务进程(爷爷进程)调用wait/waitpid函数等待爸爸进程就能立刻等待成功 此后服务进程就能继续调用accept函数获取其他客户端的连接请求
不需要等待孙子进程退出
这里主要是利用了孤儿进程的原理 当孙子进程的父进程死亡后它就会被1号进程也就是init进程领养 当孙子进程运行完毕之后它的资源会由1号进程进行回收 我们也就不需要担心僵尸进程的问题了
关闭对应的文件描述符
服务进程(爷爷进程)调用accept函数获取到新连接后 会让孙子进程为该连接提供服务,此时服务进程已经将文件描述符表继承给了爸爸进程 而爸爸进程又会调用fork函数创建出孙子进程 然后再将文件描述符表继承给孙子进程。
而父子进程创建后 它们各自的文件描述符表是独立的 不会相互影响
因此服务进程在调用fork函数后 服务进程就不需要再关心刚才从accept函数获取到的文件描述符了 此时服务进程就可以调用close函数将该文件描述符进行关闭
同样的 对于爸爸进程和孙子进程来说 它们是不需要关心从服务进程(爷爷进程)继承下来的监听套接字的 因此爸爸进程可以将监听套接字关掉
关闭文件描述符的必要性:
class TcpServer
{
public:
void Start()
{
for (;;){
//获取连接
struct sockaddr_in peer;
memset(&peer, '\0', sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_listen_sock, (struct sockaddr*)&peer, &len);
if (sock < 0){
std::cerr << "accept error, continue next" << std::endl;
continue;
}
std::string client_ip = inet_ntoa(peer.sin_addr);
int client_port = ntohs(peer.sin_port);
std::cout << "get a new link->" << sock << " [" << client_ip << "]:" << client_port << std::endl;
pid_t id = fork();
if (id == 0){ //child
close(_listen_sock); //child关闭监听套接字
if (fork() > 0){
exit(0); //爸爸进程直接退出
}
//处理请求
Service(sock, client_ip, client_port); //孙子进程提供服务
exit(0); //孙子进程提供完服务退出
}
close(sock); //father关闭为连接提供服务的套接字
waitpid(id, nullptr, 0); //等待爸爸进程(会立刻等待成功)
}
}
private:
int _listen_sock; //监听套接字
int _port; //端口号
};
运行结果如下

我们可以发现当前服务器可以支持多个客户端访问并且得到的文件描述符都是4
创建进程的成本是很高的,创建进程时需要创建该进程对应的进程控制块(task_struct)、进程地址空间(mm_struct)、页表等数据结构。而创建线程的成本比创建进程的成本会小得多,因为线程本质是在进程地址空间内运行,创建出来的线程会共享该进程的大部分资源,因此在实现多执行流的服务器时最好采用多线程进行实现
当服务进程调用accept函数获取到一个新连接后 就可以直接创建一个线程 让该线程为对应客户端提供服务
当然 主线程(服务进程)创建出新线程后 也是需要等待新线程退出的 否则也会造成类似于僵尸进程这样的问题 但对于线程来说 如果不想让主线程等待新线程退出 可以让创建出来的新线程调用pthread_detach函数进行线程分离 当这个线程退出时系统会自动回收该线程所对应的资源 此时主线程(服务进程)就可以继续调用accept函数获取新连接 而让新线程去服务对应的客户端
各个线程共享同一张文件描述符表
文件描述符表维护的是进程与文件之间的对应关系 因此一个进程对应一张文件描述符表
而主线程创建出来的新线程依旧属于这个进程 因此创建线程时并不会为该线程创建独立的文件描述符表 所有的线程看到的都是同一张文件描述符表

因此当服务进程(主线程)调用accept函数获取到一个文件描述符后 其他创建的新线程是能够直接访问这个文件描述符的
需要注意的是 虽然新线程能够直接访问主线程accept上来的文件描述符 但此时新线程并不知道它所服务的客户端对应的是哪一个文件描述符
因此主线程创建新线程后需要告诉新线程对应应该访问的文件描述符的值 也就是告诉每个新线程在服务客户端时 应该对哪一个套接字进行操作
文件描述符关闭的问题
由于此时所有线程看到的都是同一张文件描述符表 因此当某个线程要对这张文件描述符表做某种操作时 不仅要考虑当前线程 还要考虑其他线程
Service函数定义为静态成员函数
由于调用pthread_create函数创建线程时 新线程的执行例程是一个参数为void* 返回值为void*的函数 如果我们要将这个执行例程定义到类内 就需要将其定义为静态成员函数 否则这个执行例程的第一个参数是隐藏的this指针
在线程的执行例程当中会调用Service函数 由于执行例程是静态成员函数 静态成员函数无法调用非静态成员函数 因此我们需要将Service函数定义为静态成员函数 恰好Service函数内部进行的操作都是与类无关的 因此我们直接在Service函数前面加上一个static即可
Rontine函数
static void* Rontine(void* arg)
{
pthread_detach(pthread_self());
int* p = (int*)arg;
int sock = *p;
Service(sock);
return nullptr;
}
Start函数
void Start()
{
while(true)
{
// accept
struct sockaddr_in peer;
memset(&peer , '\0' , sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_sockfd , (struct sockaddr*)&peer , &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue;
}
int* p = &sock;
pthread_t tid;
pthread_create(&tid , nullptr , Rontine , (void*)p);
}
}
运行结果如下

当前多线程版的服务器存在的问题:
解决思路
我们可以发现 我们前面做的线程池可以完美解决上面的问题
服务类新增线程池成员
服务类新增线程池成员
现在当服务进程调用accept函数获取到一个连接请求后,就会根据该客户端的套接字、IP地址以及端口号构建出一个任务,然后调用线程池提供的Push接口将该任务塞入任务队列
这实际也是一个生产者消费者模型,其中服务进程就作为了任务的生产者,而后端线程池当中的若干线程就不断从任务队列当中获取任务进行处理,它们承担的就是消费者的角色,其中生产者和消费者的交易场所就是线程池当中的任务队列。
void Start()
{
_tp->ThreadPoolInit();
while(true)
{
// accept
struct sockaddr_in peer;
memset(&peer , '\0' , sizeof(peer));
socklen_t len = sizeof(peer);
int sock = accept(_sockfd , (struct sockaddr*)&peer , &len);
if (sock < 0)
{
cout << "accept error" << endl;
continue;
}
E> Task task(port);
_tp->Push(task);
}
}
设计任务类
现在我们要做的就是设计一个任务类,该任务类当中需要包含客户端对应的套接字、IP地址、端口号,表示该任务是为哪一个客户端提供服务,对应操作的套接字是哪一个。
此外,任务类当中需要包含一个Run方法,当线程池中的线程拿到任务后就会直接调用这个Run方法对该任务进行处理,而实际处理这个任务的方法就是服务类当中的Service函数,服务端就是通过调用Service函数为客户端提供服务的。
我们可以直接拿出服务类当中的Service函数,将其放到任务类当中作为任务类当中的Run方法,但这实际不利于软件分层。我们可以给任务类新增一个仿函数成员,当执行任务类当中的Run方法处理任务时就可以以回调的方式处理该任务。
Handler类
class Handler
{
Handler() = default;
void operator()(int sock)
{
cout << "get a new linl : " << sock << endl;
char buff[1024];
while(true)
{
ssize_t size = read(sock , buff , sizeof(buff) - 1);
if (size > 0)
{
buff[size] = 0; // '\0'
cout << buff << endl;
write(sock , buff , size);
}
else if (size == 0)
{
cout << "read close" << endl;
break;
}
else
{
cout << "unknown error" << endl;
}
}
close(sock);
cout << "Service end sock closed" << endl;
}
};
Task类
#pragma once
#include "sever.cc"
#include <iostream>
using namespace std;
class Task
{
private:
int _sock;
Handler _handler;
public:
Task(int sock)
:_sock(sock)
{}
Task() = default;
void run()
{
_handler(_sock);
}
};
这样子我们线程池版本的TCP网络程序就基本完成了
下面是运行结果

几个月前,我读了一篇关于rubygem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
网络编程套接字网络编程基础知识理解源`IP`地址和目的`IP`地址理解源MAC地址和目的MAC地址认识端口号理解端口号和进程ID理解源端口号和目的端口号认识`TCP`协议认识`UDP`协议网络字节序socket编程接口`sockaddr``UDP`网络程序服务器端代码逻辑:需要用到的接口服务器端代码`udp`客户端代码逻辑`udp`客户端代码`TCP`网络程序服务器代码逻辑多个版本服务器单进程版本多进程版本多线程版本线程池版本服务器端代码客户端代码逻辑客户端代码TCP协议通讯流程TCP协议的客户端/服务器程序流程三次握手(建立连接)数据传输四次挥手(断开连接)TCP和UDP对比网络编程基础知识
s=Socket.new(Socket::AF_INET,Socket::SOCK_STREAM,0)s.connect(Socket.pack_sockaddr_in('port','hostname'))ssl=OpenSSL::SSL::SSLSocket.new(s,sslcert)ssl.connect从这里开始,如果ssl连接和底层套接字仍然是ESTABLISHED,或者它是否在默认值7200之后进入CLOSE_WAIT,我想检查一个线程几秒钟甚至更糟的是在实际上不需要.write()或.read()的情况下关闭。是用select()、IO.select()还是其他方法完成
我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or
一段时间以来,我一直在使用open_uri下拉ftp路径作为数据源,但突然发现我几乎连续不断地收到“530抱歉,允许的最大客户端数(95)已经连接。”我不确定我的代码是否有问题,或者是否是其他人在访问服务器,不幸的是,我无法真正确定谁有问题。本质上,我正在读取FTPURI:defself.read_uri(uri)beginuri=open(uri).readuri=="Error"?nil:urirescueOpenURI::HTTPErrornilendend我猜我需要在这里添加一些额外的错误处理代码...我想确保我采取一切预防措施来关闭所有连接,这样我的连接就不是问题所在,但是我
我有一个super简单的脚本,它几乎包含了FayeWebSocketGitHub页面上用于处理关闭连接的内容:ws=Faye::WebSocket::Client.new(url,nil,:headers=>headers)ws.on:opendo|event|p[:open]#sendpingcommand#sendtestcommand#ws.send({command:'test'}.to_json)endws.on:messagedo|event|#hereistheentrypointfordatacomingfromtheserver.pJSON.parse(event.d
我创建了一个由于“在运行时执行的单例元类定义”而无法编码的对象(这段代码的描述是否正确?)。这是通过以下代码执行的:#defineclassXthatmyusesingletonclassmetaprogrammingfeatures#throughcallofmethod:break_marshalling!classXdefbreak_marshalling!meta_class=class我该怎么做才能使对象编码正确?是否可以从对象instance_of_x的classX中“移除”单例组件?我真的需要一个建议,因为我们的一些对象需要通过Marshal.dump序列化机制进行缓存。
我正在查看Ruby日志记录库Logging.logger方法并从sourceatgithub提出问题与这段代码有关:logger=::Logging::Logger.new(name)logger.add_appendersappenderlogger.additive=falseclass我知道类 最佳答案 这实际上删除了方法(当它实际被执行时)。这是确保close不会被调用两次的保障措施。看起来好像有嵌套的“class 关于Ruby元编程问题,我们在StackOverflow上找到一
使用Paperclip,我想从这样的URL抓取图像:require'open-uri'user.photo=open(url)问题是我最后得到一个像“open-uri20110915-4852-1o7k5uw”这样的文件名。有什么方法可以更改user.photo上的文件名?作为一个额外的变化,Paperclip将我的文件存储在S3上,所以如果我可以在初始分配中设置我想要的文件名就更好了,这样图像就会上传到正确的S3key。像这样:user.photo=open(url),:filename=>URI.parse(url).path 最佳答案