我们只能对我们熟知的东西编程,熟知必须要知道和了解。
而知识是无限扩展的未知体,了解更多也意味着更多的未知。所以需要软件工程,需要design和arch,需要高级程序员和架构师,需要项目经理,需要软件团队,需要软件方法。
当然,最需要的其实是限定问题的范围。譬如需求分析和产品计划,就是将问题简化,不必要的功能千万不要做,不做超前的计划,因为那是未知的知识的边缘。简单,一定要简单,只有简单的东西是可靠的。
软件方法中,原型一直是很好用的东西,因为原型其实省略了很多东西,只关注我们所特别关注的地方。可以对系统做原型,可以对某个问题建原型,原型解释为原始的模型最为恰当,英文为prototype。
A 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可以将千兆交换机带宽占满,错误在所难免,但它有多强悍或者多脆弱,必须了解,不能未知。
有个朋友曾经说遇到过一个奇怪的问题,交流了很久问题总结如下:
- 收到client fd,设为非阻塞,放入epoll侦听EPOLLIN事件
- client发送数据
- server端收到epoll的EPOLLIN事件,
- server写入数据(缓冲区没有满,返回值为发送数据长度)
- sleep 1秒
- 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个小时找出了问题:
结果查出来是虚拟机的问题是想把缓冲区设置为0以提高性能,当然会缓冲区满。
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;
}
注释掉,搞定,回家