linux socket手册

本文简单的介绍利用sockets进行进程间通信,本文翻译自linuxhowtos,如有纰漏, 望不吝指正。
socket翻译作套接字,这里直接使用socket。

1 客户端服务器模型(client-server model)

大部分进程间通信使用客户端-服务器模型。进程间通信指的是两个进程之间相互通信,其中,客户端进程连接服务器进程,通常是发出数据请求。一个很好的比喻是一个人给另一个人打电话,打出电话的人就好比客户端,接电话的人就好比服务器。

有两点需要注意,第一,客户端需要知道服务器是否存在,如果服务器存在,服务器的地址是多少;但是在客户端连接服务器之前,服务器并不需要知道客户端的地址(甚至客户端存在与否)。
第二,连接一旦建立,客户端和服务器之间就可以接收和发送数据。

客户端和服务器用于建立连接的系统调用有所不同,但都包含socket的基本构建。

客户端建立socket的步骤:
- 使用socket()函数(系统调用)创建一个socket
- 使用connect()函数将socket连接到服务器的地址
- 发送或接收数据。有很多种方法进行发动数据和接收数据,但是最简单的方法是使用read()和write()函数

服务器端建立socket的步骤:
- 使用socket()函数创建一个socket
- 使用bind()函数将socket和一个地址绑定,对于网络上的服务器来讲,地址包含主机和端口号,像这样:127.0.0.1:8888
- 使用listen()函数监听连接
- 当监听到有连接时,使用accept()函数接受一个连接。这个函数通常会堵塞,直到客户端连接建立。
- 接收和发送数据

2 socket类型(Socket Types)

当socket创建完之后,程序必须指定地址域和socket类型。两个进程能够相互通信,当且仅当它们的socket是相同类型的并且在相同的域。

有两种广泛使用的地址域,一种是Unix域(unix domain),两个进程共享一套文件系统进行通信,另一种是互联网域(Internet domain),两个运行在互联网主机上的进程进行通信。这两种地址域有自己的地址格式。

在Unix域中,socket的地址是文件系统中的字符串条目。

在互联网域中,socket的地址包含一个主机的网络地址(网络中的每台计算机都有一个唯一的32位地址,通常称为它的IP地址),另外,每一个socket需要一个主机的端口号。端口号是16位的无符号整形数。其中比较小的数是系统保留的,绑定一些标准服务。例如,FTP服务器的端口号是21.所有计算机上的标准服务都有相同的端口号十分重要,这样,客户端才能知道它们的地址,以方便连接。通常,大于2000的端口号是可用的。

有两种广泛使用的socket类型,一种是流socket(stream socket),另一种是数据报socket(datagram socket)。流socket把通信当做一个连续的字符串流,而数据报socket必须把整条信息一次读完。每种类型有他们自己的通信协议。

流socket使用TCP协议(transmission control protocol, 传输控制协议),TCP协议是稳定的、面向流的协议;数据报socket使用UDP协议(Unix datagram protocol),UDP协议是不稳定的,并且是面向消息的。

本手册中使用的例子使用网络域和TCP协议。

3 示例代码

提供一个简单的C程序客户端和服务器程序,程序使用网络域和TCP协议。后面详细的代码解释。在看代码解释之前,你可以先编译运行他们,看看实际效果。
文件链接:
server.c
client.c
下载文件,并分别编译成可执行文件 server 和 client (gcc xxx.c -o xxx)。

编译过程几乎不需要特殊的编译符号,不过在一些Solaris系统上,需要指定socket库,编译选项增加 -lscoket 就可以了。

如果一切正常,你就可以在网络中不同主机上分别运行server和client了(当然也可以在一台机器上运行)。首先启动server,假设server运行在本地机器上(127.0.0.1),注意,当你运行server时,你需要传递一个端口号作为参数。你可以选择2000-65535之间的任何一个数,如果碰巧那个端口被占用了,服务器会告诉你并退出,你只需要选择另一个端口号尝试,直到成功。一旦成功,服务器就会阻塞直到接收到一个客户端连接,所以,服务器什么都没有输出时不要惊慌,no news is good news.

运行服务器的命令行一般是这样的:

./server 8888

运行客户端程序时,你需要传递两个参数:主机名和端口号,像这样:

./client 127.0.0.1 8888

客户端会告诉你输入消息。如果一切顺利,服务器会输出你的消息到标准输出,然后发送一条确认消息到客户端并结束运行。

你可以在一台机器上模拟这个过程,开两个终端分别运行server和client就行了。

服务器端代码:

/* A simple server in the internet domain using TCP
   The port number is passed as an argument */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <netinet/in.h>

void error(const char *msg)
{
    perror(msg);
    exit(1);
}

int main(int argc, char *argv[])
{
     int sockfd, newsockfd, portno;
     socklen_t clilen;
     char buffer[256];
     struct sockaddr_in serv_addr, cli_addr;
     int n;
     if (argc < 2) {
         fprintf(stderr,"ERROR, no port provided\n");
         exit(1);
     }
     sockfd = socket(AF_INET, SOCK_STREAM, 0);
     if (sockfd < 0) 
        error("ERROR opening socket");
     bzero((char *) &serv_addr, sizeof(serv_addr));
     portno = atoi(argv[1]);
     serv_addr.sin_family = AF_INET;
     serv_addr.sin_addr.s_addr = INADDR_ANY;
     serv_addr.sin_port = htons(portno);
     if (bind(sockfd, (struct sockaddr *) &serv_addr,
              sizeof(serv_addr)) < 0) 
              error("ERROR on binding");
     listen(sockfd,5);
     clilen = sizeof(cli_addr);
     newsockfd = accept(sockfd, 
                 (struct sockaddr *) &cli_addr, 
                 &clilen);
     if (newsockfd < 0) 
          error("ERROR on accept");
     bzero(buffer,256);
     n = read(newsockfd,buffer,255);
     if (n < 0) error("ERROR reading from socket");
     printf("Here is the message: %s\n",buffer);
     n = write(newsockfd,"I got your message",18);
     if (n < 0) error("ERROR writing to socket");
     close(newsockfd);
     close(sockfd);
     return 0; 
}

接下来,我们将逐行解释:

#include <stdio.h>

生命了大多数输入输出的头文件,几乎被所有的C程序所包含。

#include <sys/types.h>

这个头文件包含了很多系统调用中使用的数据类型的定义。下面的两个头文件使用了这些定义。

#include <sys/socket.h>

这个头文件包含socket使用结构体的定义。

#include <netinet/in.h>

这个头文件包含了网络域中使用的常数和结构体。

 void error(char *msg)

{
  perror(msg);
  exit(1);
}

这个函数在系统调用失败的时候调用,它在标准错误上显示关于错误的消息,然后退出系统。这里有更多关于perror的信息

int main(int argc, char *argv[])
{
  int sockfd, newsockfd, portno, clilen, n;

sockfd和newsockfd是文件描述子,这两个变量存储socket和accept调用的返回值。

portno存储服务器接收连接的端口号。

clientn存储客户端地址的大小。调用accept时需要这个值。

n是read()和write()函数的返回值,就是读/写字符的数量。

char buffer[256];

服务器从socket中读取字符存储到这个数组中。

struct sockaddr_in serv_addr, cli_addr;

结构体sockaddr_in包含一个网络地址。这个结构体定义在netinet/in.h。

下面是结构体的定义

struct sockaddr_in
{
  short   sin_family; /* must be AF_INET */
  u_short sin_port;
  struct  in_addr sin_addr;
  char    sin_zero[8]; /* Not used, must be zero */
};

结构体in_addr定义在同一个头文件中,但是只包含一个域,一个叫做s_addr的unsigned long。

变量serv_addr将要保存服务器的地址,cli_addr将要保存客户端的地址。

if (argc < 2)
 {
   fprintf(stderr,"ERROR, no port provided
");
   exit(1);
 }

用户必须向程序传递一个端口号,如果不是这样,程序会给出错误提示信息。

sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
  error("ERROR opening socket");

socket()函数创建一个新的socket。它需要三个参数,第一个是socket的地址域。前面提到,有两种地址域,Unix域和网络域。符号常量AF_UNIX表示前者,AF_INET表示后者。

第二个参数是socket的类型。前面提到有两种类型,流类型(从文件或者管道中读入连续的数据流)和数据报类型(消息是按束读取的)。与之对应的符号常量分别是SOCK_STREAM和SOCK_DGAM。

第三个参数是协议。0代表操作系统会选择最合适的协议,TCP对应stream sockets,UDP对应datagram sockets。

如果系统调用失败,socket()会返回-1,这种情况下,程序会显示错误提示信息,并退出。

这只是对socket的简单描述,但是这些最常用的,还有很多其他的域和类型以供选择,参见socket() man page

bzero((char *) &serv_addr, sizeof(serv_addr));

函数bzero()将缓冲区数据设置成0,它需要两个参数,第一个是指向缓冲区的指针,第二个是缓冲区的大小。

portno = atoi(argv[1]);

把命令行传递进来的参数从string转换为int。

serv_addr.sin_family = AF_INET;

变量serv_addr是一个类型为struct sockaddr_in的结构,这个结构体包含4个域。第一个是short sin_family,表示地址族的代码,它必须被设置成 AF_INET。

serv_addr.sin_port = htons(portno);

第二个域是unsigned short sin_port, 包含一个端口名。然而,我们不能简单的复制端口号到这个域,而必须使用htons()函数把它转换成network byte order,这个函数把一个host byte order 形式的端口号转换为 network byte order形式的端口号。

serv_addr.sin_addr.s_addr = INADDR_ANY;

第三个域是类型为struct in_addr的结构体,它只包含一个unsigned long s_addr。这个域含有主机的IP地址。对于服务器代码,它永远是运行服务器程序的主机的IP地址,符号常量INADDR_ANY会获得这个地址。

if (bind(sockfd, (struct sockaddr *) &serv_addr,                   sizeof(serv_addr)) < 0)
  error("ERROR on binding");

bind()函数将socket和服务器地址绑定,它需要三个参数:socket的文件描述子、要绑定的地址、绑定地址的大小。第二个参数是指向sockaddr型结构的指针,但是传递进来的是一个sockaddr_in的结构体,所以需要进行强制类型转换。这个转换可能因为很多原因而失败,最常见的原因是socket已经被使用了,更多信息请参考 bind() manual

listen(sockfd,5);

函数listen()使进程监听连接。第一个参数是socket的文件描述子,第二个是等待队列的大小。即使第一个参数是非法的socket,函数也不会调用失败,所以代码不检查错误。

clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
  error("ERROR on accept");

accept()函数使得进程阻塞直到有客户端连接,当有客户端连接成功建立时,它会被唤醒,然后返回一个新的文件描述子,第二个参数是客户端地址的参考指针,第三个参数是结构体的大小。

bzero(buffer,256);
n = read(newsockfd,buffer,255);
if (n < 0) error("ERROR reading from socket");
printf("Here is the message: %s
",buffer);

注意,只有客户端成功连接之后,程序才会执行到这里。程序使用bzero()函数初始化缓冲区,然后从socket读取数据;读取函数使用了accept()返回的新的文件描述子,而不是最开始socket()返回的文件描述子;read()函数执行时也会发生阻塞,直到有其他的东西需要读取,即客户端执行了write()。

函数会读取socket上总的字符或者255(比实际发送的少,此时可能需要调整缓冲区的大小或修改代码),并且返回实际读取到的字符数。

n = write(newsockfd,"I got your message",18);
if (n < 0) error("ERROR writing to socket");

连接一旦建立,服务器和客户端双方都可以读取或者写入数据。自然地,客户端写的数据会被服务器读取,而服务器写的数据会被客户端读取。这段代码向客户端写入一个短消息告诉客户端:你发的数据我收到了。write()的最后一个参数表示写入消息的大小。

return 0;
}

这句代码结束main函数。

4 客户端代码

跟前面一样,我们逐句解释客户端代码。

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>

头文件跟服务器端类似,多了一个netdb.h,这个头文件中定义了hostent,后面会用到。

void error(char *msg)
{
  perror(msg);
  exit(0);
}
int main(int argc, char *argv[])
{
  int sockfd, portno, n;
  struct sockaddr_in serv_addr;
  struct hostent *server;

error()、sockfd、portno、n和之前的定义一样。serv_addr将要包含要连接的服务器地址。

变量server是一个纸箱类型为hostent结构的指针,这个结构定义在头文件netdb.h中,

struct  hostent
{
  char    *h_name;        /* official name of host */
  char    **h_aliases;    /* alias list */
  int     h_addrtype;     /* host address type */
  int     h_length;       /* length of address */
  char    **h_addr_list;  /* list of addresses from name server */
  #define h_addr  h_addr_list[0]  /* address, for backward compatiblity */
};

它定义了一个网络上的主机,结构的成员是

h_name       Official name of the host.
h_aliases    A zero  terminated  array  of  alternate
             names for the host.
h_addrtype   The  type  of  address  being  returned;
             currently always AF_INET.
h_length     The length, in bytes, of the address.
h_addr_list  A pointer to a list of network addresses
             for the named host.  Host addresses are
             returned in network byte order.

注意h_addr是网络地址的第一个元素的别名。

char buffer[256];
if (argc < 3)
{
  fprintf(stderr,"usage %s hostname port
", argv[0]);
  exit(0);
}
portno = atoi(argv[2]);
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0)
  error("ERROR opening socket");

这些和服务器端一致。

server = gethostbyname(argv[1]);
if (server == NULL)
{
  fprintf(stderr,"ERROR, no such host
");
  exit(0);
}

变量argv[1]是传递进来的主机IP地址,函数

struct hostent *gethostbyname(char *name)

输出参数是一个主机名字,返回一个指向hostent的指针,包含主机的信息。域char *h_addr包含IP地址。

如果结构是NULL,那么系统不能根据名字定位主机。

以前,这个函数通过查找系统文件/etc/hosts,但是,随着互联网的增长,系统不可能维护这个文件。因此,这个函数的工作原理变得复杂,可能要查询整个国家的数据库。

bzero((char *) &serv_addr, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
bcopy((char *)server->h_addr,
      (char *)&serv_addr.sin_addr.s_addr,
      server->h_length);
serv_addr.sin_port = htons(portno);

这段代码设置serv_addr,基本和服务器端相同,但是,因为server->h_addr是一个字符串,我们实用函数:

void bcopy(char *s1, char *s2, int length)

将从s1中复制length字节的数据到s2。

if (connect(sockfd,&serv_addr,sizeof(serv_addr)) < 0)
  error("ERROR connecting");

客户端调用connec()函数建立到服务器的连接。connect函数需要三个参数:socket的文件描述子、要连接的主机地址、地址的大小。函数执行成功返回0,否则返回-1.

注意,客户端需要知道服务器的端口,但是它不需要知道它自己的端口号。

printf("Please enter the message: ");
  bzero(buffer,256);
  fgets(buffer,255,stdin);
  n = write(sockfd,buffer,strlen(buffer));
  if (n < 0)
    error("ERROR writing to socket");
  bzero(buffer,256);
  n = read(sockfd,buffer,255);
  if (n < 0)
    error("ERROR reading from socket");
  printf("%s
",buffer);
  return 0;
}

剩下的代码很简单,它提示用户输入消息,使用fgets()函数从标准输入读取消息,并写入socket,然后读取socket上的回复信息,并显示在屏幕上。

5 服务器端代码完善

前面说的简单的服务器代码有一个限制:它只处理一个连接,然后退出。一个实际的服务器应该无限期的运行,并且具有处理并发连接的能力,每一个连接都有它自己的进程。这通常需要fork一个新的进程处理每一个新的连接。

下面的代码包含一个哑函数:dostuff(int sockfd)。当连接建立后,这个函数会起作用,并且提供客户端请求的服务。像我们前面说的,一旦连接建立,客户端和服务器都可以使用read()和write()函数收发信息。

要写出一个实际的服务器,main()函数基本不需要改变,提供服务的代码最好全部写在dostuff()函数中。

为了允许服务器多个并发连接,我们对代码做出如下改进:
- 将accept语句放在一个无限循环内
- 建立连接后,调用fork()函数创建新的进程
- 子进程会关闭sockfd,并调用dostuff,接受新的socket文件描述子为参数。当两个进程会话完毕,即dostuff()返回时,进程不销毁
- 父进程关闭了newsockfd,因为所有的代码都是在一个无限循环中,newsockfd还要等待下一个连接

代码:

while (1)
 {
   newsockfd = accept(sockfd,
               (struct sockaddr *) &cli_addr, &clilen);
   if (newsockfd < 0)
     error("ERROR on accept");
   pid = fork();
   if (pid < 0)
     error("ERROR on fork");
   if (pid == 0)
   {
     close(sockfd);
     dostuff(newsockfd);
     exit(0);
   }
   else
     close(newsockfd);
 } /* end of while */

完整代码

6 僵尸进程问题(参见原文)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值