使用原型——不要做躺在炸弹上的勇士

时间:2022-01-22 03:32:39

我们只能对我们熟知的东西编程,熟知必须要知道和了解。

而知识是无限扩展的未知体,了解更多也意味着更多的未知。所以需要软件工程,需要design和arch,需要高级程序员和架构师,需要项目经理,需要软件团队,需要软件方法。

当然,最需要的其实是限定问题的范围。譬如需求分析和产品计划,就是将问题简化,不必要的功能千万不要做,不做超前的计划,因为那是未知的知识的边缘。简单,一定要简单,只有简单的东西是可靠的。

软件方法中,原型一直是很好用的东西,因为原型其实省略了很多东西,只关注我们所特别关注的地方。可以对系统做原型,可以对某个问题建原型,原型解释为原始的模型最为恰当,英文为prototype。

prototype is an early sample or model built to test a concept or process or to act as a thing to be replicated or learned from.(Wikipedia)

原型,就是最简单的,为了验证需求或者关键技术的最简模型。

若对未知的东西进行编程,会有什么样的后果?假设我们不知道一个系统是不是我们想要的,假设我们遇到一个问题最后解决了但不确定是什么问题,假设我们碰到一个从未用过的技术,这些都是对未知的东西进行编程,会有什么后果?

就是人月神话描述的那个焦油坑。而且更加悄无声色——只是那些程序员有改不完的bug,有无止境的需求修改,每天什么代码也写不了因为整天都有人找麻烦——就像一个隐形炸弹。

如果放置了一个隐形炸弹,不知道它什么时候爆炸,或许还好一点,可惜大家大多是放置炸弹的高手,你放一个我放一个,谁被炸死谁倒霉。

不要做躺在炸弹上的勇士,使用原型。

我喜欢对新技术使用原型,譬如我第一个服务器,我没有做过服务器,总有第一次的,所以我花了1个月时间做出来原型,只能传输数据,但我知道可行。譬如多进程,信号,epoll,socketpair,zombie。。。我不知道的任何东西都有原型。

我会对错误使用原型,譬如tcmalloc检测内存越界,非虚的析构函数导致内存泄漏,那些花了很长时间找到的错误,我都需要建立原型,来证明确实是这么错的,而不是侥幸纠正了问题。

原型让我对socket的不那么有信心,知道socket可以将千兆交换机带宽占满,错误在所难免,但它有多强悍或者多脆弱,必须了解,不能未知。

有个朋友曾经说遇到过一个奇怪的问题,交流了很久问题总结如下:

  1. 收到client fd,设为非阻塞,放入epoll侦听EPOLLIN事件
  2. client发送数据
  3. server端收到epoll的EPOLLIN事件,
  4. server写入数据(缓冲区没有满,返回值为发送数据长度)
  5. sleep 1秒
  6. server连续写入3个数据,都不大,这时候会收到EAGAIN。

我坚信收到EAGAIN缓冲区就满了,他认为sleep了1秒或100秒之后缓冲区肯定不会满(那3个包很小)。好吧,我说我坚信,如果三个包很小必定不会EAGAIN;如果有EAGAIN一定满了。

我们争论不休,所以我写了如下的原型:

// simple-server: to test the epoll in event.
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <iostream>
using namespace std;

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <sys/epoll.h>
#include <string.h>
#include <errno.h>

#define err_exit(msg) cout << "[error] " << msg << endl; exit(1)

struct UserOptions{
int port;
int packet_size;
};

void discovery_user_options(int argc, char** argv, UserOptions& options){
if(argc <= 2){
cout << "Usage: " << argv[0] << " <port> <packet_size>" << endl
<< "port: the port to listen" << endl
<< "packet_size: the packet size in b. must >= 8" << endl
<< "For example: " << argv[0] << " 1990 1000" << endl;
exit(1);
}

options.port = atoi(argv[1]);
options.packet_size = atoi(argv[2]);
assert(options.port > 0);
assert(options.packet_size >= 8);
}

int listen_server_socket(UserOptions& options){
int serverfd = socket(AF_INET, SOCK_STREAM, 0);

if(serverfd == -1){
err_exit("init socket error!");
}
cout << "init socket success! #" << serverfd << endl;

int reuse_socket = 1;
if(setsockopt(serverfd, SOL_SOCKET, SO_REUSEADDR, &reuse_socket, sizeof(int)) == -1){
err_exit("setsockopt reuse-addr error!");
}
cout << "setsockopt reuse-addr success!" << endl;

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(options.port);
addr.sin_addr.s_addr = INADDR_ANY;
if(bind(serverfd, (const sockaddr*)&addr, sizeof(sockaddr_in)) == -1){
err_exit("bind socket error!");
}
cout << "bind socket success!" << endl;

if(listen(serverfd, 10) == -1){
err_exit("listen socket error!");
}
cout << "listen socket success! " << options.port << endl;

return serverfd;
}

void epoll_add_in_event(int ep, int fd){
epoll_event ee;
ee.events = EPOLLIN;
ee.data.fd = fd;

if(epoll_ctl(ep, EPOLL_CTL_ADD, fd, &ee) == -1) exit(-1);
}

int main(int argc, char** argv){
UserOptions options;
discovery_user_options(argc, argv, options);
int serverfd = listen_server_socket(options);

int ep = epoll_create(1024);
if(ep == -1)exit(-1);
epoll_add_in_event(ep, serverfd);

for(;;){
epoll_event events[1024];
int incoming = epoll_wait(ep, events, 1024, -1);
if(incoming == -1) exit(-1);

for(int i = 0; i < incoming; i++){
epoll_event active = events[i];

int fd = active.data.fd;
bool write_event = (active.events & EPOLLOUT) == EPOLLOUT;
bool event_error = (active.events & EPOLLHUP) == EPOLLHUP || (active.events & EPOLLERR) == EPOLLERR;
printf("get a epoll event, fd=%d, is_hup=%d, is_write=%d\n", fd, event_error, write_event);

if(fd == serverfd){
int client = accept(serverfd, NULL, 0);
cout << "client incoming: #" << client << endl;
if(client == -1) exit(-1);

int flag = 1;
if(ioctl(client, FIONBIO, &flag) == -1) exit(-1);

epoll_add_in_event(ep, client);
}
else{
int client = fd;
cout << "client data: #" << client << endl;

int size = options.packet_size / 8;
char* data = new char[size];

int ret = send(client, data, size, 0);
cout << "ret = " << ret << ", errno=" << errno << endl;

sleep(1);
int ret1 = send(client, data, size, 0);
cout << "ret1 = " << ret1 << ", errno=" << errno << endl;
int ret2 = send(client, data, size, 0);
cout << "ret2 = " << ret2 << ", errno=" << errno << endl;
int ret3 = send(client, data, size, 0);
cout << "ret3 = " << ret2 << ", errno=" << errno << endl;

close(client);
delete[] data;
}
}
}

return -1;
}

// simple-client to test epoll in.
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <assert.h>
using namespace std;

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#define err_exit(msg) cout << "[error] " << msg << endl; exit(1)

struct UserOptions{
char* server_ip;
int port;
};

void discovery_user_options(int argc, char** argv, UserOptions& options){
if(argc <= 2){
cout << "Usage: " << argv[0] << " <server_ip> <port>" << endl
<< "server_ip: the ip address of server" << endl
<< "port: the port to connect at" << endl
<< "For example: " << argv[0] << " 192.168.100.145 1990" << endl;
exit(1);
}

options.server_ip = argv[1];
options.port = atoi(argv[2]);
assert(options.port > 0);
}

int connect_server_socket(UserOptions& options){
int clientfd = socket(AF_INET, SOCK_STREAM, 0);

if(clientfd == -1){
err_exit("init socket error!");
}
cout << "init socket success!" << endl;

sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(options.port);
addr.sin_addr.s_addr = inet_addr(options.server_ip);
if(connect(clientfd, (const sockaddr*)&addr, sizeof(sockaddr_in)) == -1){
err_exit("connect socket error!");
}
cout << "connect socket success!" << endl;

return clientfd;
}

int main(int argc, char** argv){
UserOptions options;
discovery_user_options(argc, argv, options);

int client = connect_server_socket(options);

const char* msg = "hello, server";
send(client, msg, strlen(msg), 0);

int size = 100 /*kbps*/ * 1000 / 8;
char* buf = new char[size];
while(true){
if(recv(client, buf, size, 0) <= 0){
close(client);
err_exit("client recv error!");
}
}

return 0;
}

运行结果显示:

[winlin@dev6 2012-8-10-epoll-out]$ ./server 1991 1000
init socket success! #3
setsockopt reuse-addr success!
bind socket success!
listen socket success! 1991
get a epoll event, fd=3, is_hup=0, is_write=0
client incoming: #5
get a epoll event, fd=5, is_hup=0, is_write=0
client data: #5
ret = 125, errno=0
ret1 = 125, errno=0
ret2 = 125, errno=0
ret3 = 125, errno=0

说明四个包都正常发送。所以必定是他的代码在其他的地方有问题,他决定要找出来为什么会不同的结果。

如果是我错了呢?有什么关系呢?被错误打击一次,和被炸弹炸一次,我选择前者。

如果他用其他的方式绕过这个问题呢?他放置了一个隐形炸弹,未知的炸弹。

不要做躺在炸弹上的勇士。

据说这个问题查了三天,这个原型确定了是他的代码的问题,所以他花了5个小时找出了问题:

结果查出来是虚拟机的问题
int32_t iOptValue = 0;
if (0 != setsockopt(fd, SOL_SOCKET, SO_SNDBUF, (const void *)&iOptValue, (socklen_t)sizeof(iOptValue))){
return -1;
}
if (0 != setsockopt(fd, SOL_SOCKET, SO_RCVBUF, (const void *)&iOptValue, (socklen_t)sizeof(iOptValue))){
return -1;
}
注释掉,搞定,回家
是想把缓冲区设置为0以提高性能,当然会缓冲区满。