下面的例子示意了服务端使用多线程来处理socket请求的例子。
在《Unix网络编程》中称这种方式为网络服务器。要区别“监听套接字”和“已连接套接字”
原文在这里:http://www.wangafu.net/~nickm/libevent-book/01_intro.html#_footnote_1
多线程处理的不利之处在于:
1、创建线程开销较大。
2、线程池不易扩展。
3、如果要处理成千上百个连接,创建这么多线程时系统承受不了
/* For sockaddr_in */
#include <netinet/in.h>
/* For socket functions */
#include <sys/socket.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#define MAX_LINE 16384
//将一个字符转化成对应的rot13字符,是一种简单的加密方式,这里用这个干嘛?
//A换成N,B换成O,以此类推,M换成Z,然后序列反转,N换成A,O换成B...
char rot13_char(char c) {
/* We don't want to use isalpha here; setting the locale would change
* which characters are considered alphabetical. */
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
//在子进程里执行这个函数
void child(int fd) {
//缓冲区
char outbuf[MAX_LINE + 1];
size_t outbuf_used = 0; //size_t long unsiged int
ssize_t result; //ssize_t long int
while (1) {
char ch;
result = recv(fd, &ch, 1, 0); //第三个参数为len,第四个参数为符号位,一般设置为0。这里一次只读一个字符
if (result == 0) { //返回0表示读完了
break;
} else if (result == -1) {
perror("read");
break;
}
/* We do this test to keep the user from overflowing the buffer. */
if (outbuf_used < sizeof(outbuf)) {
outbuf[outbuf_used++] = rot13_char(ch); //这里将字符转码
}
//当遇到换行符的时候,将编码后的结果返回给客户端
if (ch == '\n') {
send(fd, outbuf, outbuf_used, 0);
outbuf_used = 0;
continue;
}
}
}
void run(void) {
int listener;
struct sockaddr_in sockAddr_in;
sockAddr_in.sin_family = AF_INET;
sockAddr_in.sin_addr.s_addr = 0; //所以服务端socket地址就是设置为0
sockAddr_in.sin_port = htons(40713); //直接指定端口号
listener = socket(AF_INET, SOCK_STREAM, 0);
//如果不是WIN32系统,则做下面的事
#ifndef WIN32
{
int one = 1;
// 我们知道,在TCP断开链接的时候我们需要四次握手来断开,而且当两端都关闭了read/write通道以后我们还是要等待一个TIME_WAIT时间。
// 这就是SO_REUSEADDR的作用所在.
// 其实这个选项就是告诉OS如果一个端口处于TIME_WAIT状态, 那么我们就不用等待直接进入使用模式, 不需要继续等待这个时间结束.
//参考地址:http://www.cnblogs.com/linehrr-freehacker/p/3309156.html
// SO_REUSEADDR可以用在以下四种情况下。
// (摘自《Unix网络编程》卷一,即UNPv1)
// 1、当有一个有相同本地地址和端口的socket1处于TIME_WAIT状态时,而你启
// 动的程序的socket2要占用该地址和端口,你的程序就要用到该选项。
// 2、SO_REUSEADDR允许同一port上启动同一服务器的多个实例(多个进程)。但
// 每个实例绑定的IP地址是不能相同的。在有多块网卡或用IP Alias技术的机器可
// 以测试这种情况。
// 3、SO_REUSEADDR允许单个进程绑定相同的端口到多个socket上,但每个soc
// ket绑定的ip地址不同。这和2很相似,区别请看UNPv1。
// 4、SO_REUSEADDR允许完全相同的地址和端口的重复绑定。但这只用于UDP的
// 多播,不用于TCP。
setsockopt(listener, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
}
#endif
if (bind(listener, (struct sockaddr*) &sockAddr_in, sizeof(sockAddr_in)) < 0) {
perror("bind");
return;
}
//对于服务端程序,使用bind()绑定套接字之后,还需要使用listen函数让套接字进入被动监听状态
//再调用accept()函数,就可以随时响应客户端的请求了
//第二个参数为请求队列的最大长度
//参考:http://c.biancheng.net/cpp/html/3036.html
//当套接字正在处理客户端请求时,如果有新的请求进来,套接字是没法处理的,
//只能把它放进缓冲区,待当前请求处理完毕后,再从缓冲区中读取出来处理。
//如果不断有新的请求进来,它们就按照先后顺序在缓冲区中排队,直到缓冲区满。这个缓冲区,就称为请求队列(Request Queue)。
//一个服务器通常仅仅创建一个监听套接字,他在该服务器的生命周期内一直存在。“监听套接字”
if (listen(listener, 16) < 0) {
perror("listen");
return;
}
while (1) {
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
//如果accept成功,那么其返回值是由内核自动生成的一个全新描述符,代表与所返回客户的TCP连接;返回“已连接套接字”
int connfd = accept(listener, (struct sockaddr*) &ss, &slen);
if (fd < 0) {
perror("accept");
} else {
//fork之前,只有一个进程在执行这段代码,但在执行这条语句后,就变成两个进程在执行了,这两个进程几乎完全相同
//,将要执行的下一条语句都是 if(pid==0)。这两个进程的执行没有先后顺序,靠系统的进程调度策略
int pid = fork();
if (pid == 0) {
child(connfd);
exit(0);
}
}
//在这个地方关闭fd并没有断开socket连接,因为子进程也有这个socket的复制,
//这个socket的引用计数是2,这里调用close将其计数减小为1
close(connfd);
}
}
int main(int c, char **v) {
run();
return 0;
}
整个过程连接的状态变化图如下,connfd表示新的socket连接: