这是关于怎么用各种能够得到的接口为Linux开发网络程序的系列文章的第一篇。就像大多数Unix-based的操作体系相同,Linux支撑将TCP/IP作为本地的网络传输协议。在这个系列中,咱们假定你现已比较了解Linux上的C编程和Linux的一些体系常识比方signals,forking等等。
这篇文章是关于怎么用BSD套接口创立网络程序的根底介绍 。鄙人一篇中,咱们会处理涉及到树立(网络)deamon进程的问题。并且往后的文章咱们还会涉及到运用远程过程调用(RPC),以及用CORBA/distributed objects进行开发。
一、TCP/IP的根底介绍
TCP/IP协议族答应两个运行在同一台电脑或许由网络衔接在一起的两台电脑上的程序进行通讯。这个协议族是专门为了在不牢靠的网络上进行通讯规划的。TCP/IP答应两个根本的操作方法——面向衔接的牢靠的传输(指TCP)和无衔接的(connectionless)不牢靠的传输(UDP)。
TCP供给带有对上层协议通明的中继功用的,次序的,牢靠的,双向的(bi-directional),以衔接为根底的字节传输流。TCP将你的信息切割成数据报(不大于64kb)并确保一切的数据报无误的依照次序都抵达意图地。因为以衔接为根底,所以一个虚拟衔接有必要在一个网络实体(network entity)和另一个之间进行通讯前树立。UDP相反则供给一个(非常快的)无衔接的不牢靠音讯传输(音讯的巨细是一个确认的最大长度)。
为了使程序间能够彼此通讯,不管他们是在同一个机器(经过loopback接口)仍是不同主机,每一个程序都有必要有独立的地址。
TCP/IP地址由两部分组成——用来区分机器的IP地址和用来区分在那台机器上的特定程序的端口地址。
地址能够是点分(dotted-quad)符号方法的(如,127.0.0.1)或许是主机名方法的(如,www.csdn.net)。体系能够运用/etc/hosts或DNS域名服务(假如能够取得的话)进行主机名到点分符号地址(也便是IP地址)的转化。
端口从1号开端编号。1和IPP0RT_RESERVED(在/usr/include/netinet/in.h中界说,一般为1024)之间的段标语保留给体系运用(也便是说,你有必要以root的身份树立一个网络服务来绑定这部分的端口)。
最简略的网络程序大都用的客户-服务器模型。一个服务进程等候一个客户进程衔接他。当衔接树立时,服务器代表客户履行特定的使命,一般这这今后衔接就中止了。
二、运用BSD套接口界面
最通行的TCP/IP编程办法便是运用BSD套接口界面编程。经过它,网络端点(network endpoints)(IP地址和端口地址)以套接口(sockets)的方法呈现。
这套套接口IPC(interprocess communication,进程间通讯)设备(从4.2BSD开端引进)的规划是为了能让网络程序的规划能够独立于不同的底层通讯设备。
1、树立一个服务器程序
要运用BSD界面树立一个服务器程序,你有必要经过以下过程:
(1)经过函数socket()树立一个套接口
(2)经过函数bind()绑定一个地址(IP地址和端口地址)。这一步确认了服务器的方位,使客户端知道怎么拜访。
(3)经过函数listem()监听(listen)端口的新的衔接恳求。
(4)经过函数accept()承受新的衔接。
一般,保护代表了客户的恳求或许需求花费适当长的一段时刻。在处理一个恳求时,接纳和处理新的恳求也应该是高效的。抵达这种意图的最一般的做法是让服务器经过fork()函数复制一份自己的进程来承受新的衔接。
以下的比方显现了服务器是怎么用C完成的:
/*
* Simple Hello, World! server
* Ivan Griffin (ivan.griffin@ul.ie)
*/
/* Hellwolf Misty translated */
#include /* */
#include /* exit() */
#include /* memset(), memcpy() */
#include /* uname() */
#include
#include /* socket(), bind(),
listen(), accept() */
#include
#include
#include
#include /* fork(), write(), close() */
/*
* constants
*/
const char MESSAGE[] = Hello, World!n;
const int BACK_LOG = 5;
/*
*程序需求一个命令行参数:需求绑定的端标语
*/
int main(int argc, char *argv[])
{
int serverSocket = 0,
on = 0,
port = 0,
status = 0,
childPid = 0;
struct hostent *hostPtr = NULL;
char hostname[80] = ;
struct sockaddr_in serverName = { 0 };
if (2 != argc)
{
fprintf(stderr, Usage: %s n,
argv[0]);
exit(1);
}
port = atoi(argv[1]);
/ *
*socket()体系调用,带有三个参数:
* 1、参数domain指明通讯域,如PF_UNIX(unix域),PF_INET(IPv4),
* PF_INET6(IPv6)等
* 2、type指明通讯类型,最常用的如SOCK_STREAM(面向衔接牢靠方法,
* 比方TCP)、SOCK_DGRAM(非面向衔接的非牢靠方法,比方UDP)等。
* 3、参数protocol指定需求运用的协议。尽管能够对同一个协议
* 宗族(protocol family)(或许说通讯域(domain))指定不同的协议
* 参数,可是一般只需一个。关于TCP参数可指定为IPPROTO_TCP,关于
* UDP能够用IPPROTO_UDP。你不用显式拟定这个参数,运用0则依据前
* 两个参数运用默许的协议。
*/
serverSocket = socket(PF_INET, SOCK_STREAM,
IPPROTO_TCP);
if (-1 == serverSocket)
{
perror(socket());
exit(1);
}
/*
* 一旦套接口被树立,它的运作机制能够经过套接口选项(socket option)进行修正。
*/
/*
* SO_REUSEADDR选项的设置将套接口设置成从头运用旧的地址(IP地址加端标语)而不等候
* 留意:在Linux体系中,假如一个socket绑定了某个端口,该socket正常封闭或程序退出后,
* 在一段时刻内该端口仍然坚持被绑定的状况,其他程序(或许从头启动 的原程序)无法绑定该端口。
*
* 下面的调用中:SOL_SOCKET代表对SOCKET层进行操作
*/
on = 1;
status = setsockopt(serverSocket, SOL_SOCKET,
SO_REUSEADDR,
(const char *) on, sizeof(on));
if (-1 == status)
{
perror(setsockopt(…,SO_REUSEADDR,…));
}
/* 当衔接中止时,需求推迟封闭(linger)以确保一切数据都
* 被传输,所以需求翻开SO_LINGER这个选项
* linger的结构在/usr/include/linux/socket.h中界说:
* struct linger
* {
* int l_onoff; /* Linger active */
* int l_linger; /* How long to linger */
* };
* 假如l_onoff为0,则推迟封闭特性就被撤销。假如非零,则答应套接口推迟封闭。
* l_linger字段则指明推迟封闭的时刻
*/
{
struct linger linger = { 0 };
linger.l_onoff = 1;
linger.l_linger = 30;
status = setsockopt(serverSocket,
SOL_SOCKET, SO_LINGER,
(const char *) linger,
sizeof(linger));
if (-1 == status)
{
perror(setsockopt(…,SO_LINGER,…));
}
}
/*
* find out who I am
*/
status = gethostname(hostname,
sizeof(hostname));
if (-1 == status)
{
perror(gethostname());
exit(1);
}
hostPtr = gethostbyname(hostname);
if (NULL == hostPtr)
{
perror(gethostbyname());
exit(1);
}
(void) memset(serverName, 0,
sizeof(serverName));
(void) memcpy(serverName.sin_addr,
hostPtr->h_addr,
hostPtr->h_length);
/*
*h_addr是h_addr_list[0]的近义词,
* h_addr_list是一组地址的数组
*长度为4(byte)代表一个IP地址的长度
*/
/*
* 为了使服务器绑定本机一切的IP地址,
* 上面一行代码需求用下面的代码替代
* serverName.sin_addr.s_addr=htonl(INADDR_ANY);
*/
serverName.sin_family = AF_INET;
/* htons:h(host byteorder,主机字节序)
* to n(network byteorder,网络字节序
* s(short类型)
*/
serverName.sin_port = htons(port);
/* 在一个地址(本例中的serverSocket)被树立后
* 它就应该被绑定到咱们取得的套接口。
*/
status = bind(serverSocket,
(struct sockaddr *) serverName,
sizeof(serverName));
if (-1 == status)
{
perror(bind());
exit(1);
}
/* 现在套接口就能够被用来监听新的衔接。
* BACK_LOG指定了未决衔接监听行列(listen queue for pending connections)
* 的最大长度。当一个新的衔接抵达,而行列已满的话,客户就会得到衔接回绝过错。
* (这便是dos回绝服务进犯的根底)。
*/
status = listen(serverSocket, BACK_LOG);
if (-1 == status)
{
perror(listen());
exit(1);
}
/* 从这儿开端,套接口就开端预备承受恳求,并为他们服务。
* 本比方是用for循环来抵达这个意图。一旦衔接被承受(accpepted),
* 服务器能够经过指针取得客户的地址以便进行一些比方记载客户登陆之类的
* 使命。
for (;;)
{
struct sockaddr_in clientName = { 0 };
int slaveSocket, clientLength =
sizeof(clientName);
(void) memset(clientName, 0,
sizeof(clientName));
slaveSocket = accept(serverSocket,
(struct sockaddr *) clientName,
clientLength);
if (-1 == slaveSocket)
{
perror(accept());
exit(1);
}
childPid = fork();
switch (childPid)
{
case -1: /* ERROR */
perror(fork());
exit(1);
case 0: /* child process */
close(serverSocket);
if (-1 == getpeername(slaveSocket,
(struct sockaddr *) clientName,
clientLength))
{
perror(getpeername());
}
else
{
printf(Connection request from %sn,
inet_ntoa(clientName.sin_addr));
}
/*
* Server application specific code
* goes here, e.g. perform some
* action, respond to client etc.
*/
write(slaveSocket, MESSAGE,
strlen(MESSAGE));
/* 也能够运用带缓存的ANSI函数fprint,
* 只需你记住必要时用fflush改写缓存
*/
close(slaveSocket);
exit(0);
default: /* parent process */
close(slaveSocket);/* 这是一个非常好的习气
* 父进程封闭子进程的套接口描述符
* 正如上面的子进程封闭父进程的套接口描述符。
*/
}
}
return 0;
}