socket基本用法

本文参考: http://www.linuxhowtos.org/C_C++/socket.htm, 仅供学习用途

客户端(Client)/服务端(Server)模型

客户端/服务端模型是进程间通信中使用的比较广泛的一种模型。客户端/服务端模型指的是一种两个进程互相通信的一种方式。对此通信方式的一个很好的比喻就是顾客给客服打电话(顾客是客户端,客服是服务端)。值得一提的是,客户端在建立连接时需要知道服务端的地址,而服务端在连接建立之前是不需要知道客户端地址的,客户端和服务端之间可以互相发送消息。

在客户端建立socket的步骤如下:

  • 使用系统调用socket()创建一个socket
  • 使用系统调用connect()连接至指定地址的服务器
  • 发送和接收数据,有许多发送和接收的方法,最简单的是系统调用read()和write()

在服务端建立socket的步骤如下:

  • 使用系统调用socket()创建一个socket
  • 使用bind()绑定一个地址到socket上,对网络上的socket来说,地址包括了地址名和端口号
  • 使用listen()监听连接
  • 使用accept()接受连接,此调用通常会阻塞知道有客户端连接值服务器
  • 发送和接收数据

Socket的类型

Socket在创建时需要指定地址域和类型。只有当两个进程的地址和类型都相同时他们才可能进行通信。

两种常见的地址域包括Unix域和Internet域,Unix域将socket当做文件系统下的一个文件,而Internet域适用于不同主机中的进程通信。

需要注意的是,在Internet域下,socket需要指定地址和端口号,而在Unix下一些低数量的端口号是被Unix系统所占用了的,我们必须避免这种冲突,一般来说,2000以上的端口号是可以用的。

有两种十分常见的Socket类型,他们就是流类型(stream sockets)和数据报类型(datagram socekts)。流类型用于可以实现连续信息的传递,使用TCP连接,而数据报类型用于单个信息的传递,使用UDP连接。

本文的代码使用的是流类型的socket,使用tcp协议。

示例

本文使用的代码包括两个文件:

使用:

  • 编译后首先运行服务端程序./server 51717
  • 使用客户端进行连接./client localhost 51717
  • 客户端可以发送文字消息至服务端

服务端代码的改进

示例中的服务器段代码仅仅接收一次消息程序便将退出,而在实际中,通常服务器将接收多个消息,并且针对接收到的消息会做一些事情,所以我们有必要对此做出一些改进。

改进如下:

  • 讲accept()语句放入一个无限循环中
  • 当连接建立后,使用fork()新建一个进程
  • 子进程将关闭sock的文件描述符,并且完成指定任务,然后退出
  • 父进程也关闭sock的文件描述符,执行下一个循环开始监听消息

代码如下:

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 */

查看改进后的服务器端代码。

僵尸进程问题

上述程序有一个问题,当父进程运行一段时间并接受到很多连接请求后,每个连接请求都会创建一个子进程,而当子进程结束后,由于父进程没有是用wait对子进程信息进行捕获处理,子进程的进程描述符将仍然保存在系统里,并变成了僵尸进程。僵尸进程对系统有许多坏处,我们必须避免这种情况。但是如果在每个循环中都加一个wait(),这将是循环变得堵塞,程序在没处理完当前请求对应的任务之前不能开始下一个消息的接收。一个可行的解决方案是,当子进程结束时,它将发送一个SIGCHLD信号至父进程,我们可以捕获这个信号,并执行我们自定义的信号处理函数,在此函数中对子进程进行处理,设置自定义信号处理函数的代码如下:

signal(SIGCHLD, sig_chld);

此段代码意为每次接收到SIGCHLD信号时将触发sig_chld函数。其他代码如下:

void sig_chld(int signo)//信号处理函数必须要有一个int 型的参数,并且返回值为void
{
pid_t pid;
int stat;

while((pid = waitpid(-1, &stat, WNOHANG)) > 0)
{
printf("child %d terminated\n", pid);
}
return;
}
...
int main()
{
...
signal(SIGCHLD, sig_chld);
...

此时每次父进程接受到SIGCHLD信号时,将会触发sig_chld函数,函数中将使用waitpid()对子程序进行清理。WNOHANG标志位的设定代表这个wait是无堵塞的。

其他类型的socket

除了使用流类型的socket,socekt还支持数据报类型,它使用UDP协议进行通信,他与流类型的socket的差别主要包括:

  • 数据报类型的socekt是不可靠的,协议并不保证接收方能接受到来自发送方的消息
  • 消息被保存在数据报内,这意味这如果发送方发送的是一个100比特的数据报,接受方收到的也会是100比特的数据报。而在流类型的socket中,如果发送方发送了100比特的数据,接收方可能收到两个50比特的消息块或者1个100比特的消息块
  • 通信使用的系统调用是sendto()receivefrom(),而不是通常的read()write()
  • 由于数据报socket不需要建立持续性的连接,他的开销会小很多,这也是数据报socket在一些要求相应速度快的地方得到了广泛应用
  • 使用数据报socekt实现的服务端代码
  • 使用数据报socekt实现的客户端代码

Unix域下的socket

Unix域下的socket与Internet域下唯一的不同是地址的形式。以下是Unix域地址格式:

struct	sockaddr_un
{
short sun_family; /* AF_UNIX */
char sun_path[108]; /* path name (gag) */
};

sun_path是unix文件系统的路径名,显然客户端和服务端必须在同一主机下。当socket建立后,将会在文件系统中创建一个这样的文件,Unix域下的Socket和命名管道(FIFOs)是基本等价的。

示例代码如下:

  • Unix域下的socket服务端代码
  • Unix域下的socket客户端代码

原始代码参考解释

Server code

#include <stdio.h>

This header file contains declarations used in most input and output and is typically included in all C programs.

#include <sys/types.h>

This header file contains definitions of a number of data types used in system calls. These types are used in the next two include files.

#include <sys/socket.h>

The header file socket.h includes a number of definitions of structures needed for sockets.

#include <netinet/in.h>

The header file in.h contains constants and structures needed for internet domain addresses.

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

This function is called when a system call fails. It displays a message about the error on stderr and then aborts the program. The perror man page gives more information.

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

sockfd and newsockfd are file descriptors, i.e. array subscripts into the file descriptor table . These two variables store the values returned by the socket system call and the accept system call.
portno stores the port number on which the server accepts connections.

clilen stores the size of the address of the client. This is needed for the accept system call.

n is the return value for the read() and write() calls; i.e. it contains the number of characters read or written.

char buffer[256];

The server reads characters from the socket connection into this buffer.

struct sockaddr_in serv_addr, cli_addr;

A sockaddr_in is a structure containing an internet address. This structure is defined in netinet/in.h.

Here is the definition:

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 */
};

An in_addr structure, defined in the same header file, contains only one field, a unsigned long called s_addr.
The variable serv_addr will contain the address of the server, and cli_addr will contain the address of the client which connects to the server.

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

The user needs to pass in the port number on which the server will accept connections as an argument. This code displays an error message if the user fails to do this.

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

The socket() system call creates a new socket. It takes three arguments. The first is the address domain of the socket.
Recall that there are two possible address domains, the unix domain for two processes which share a common file system, and the Internet domain for any two hosts on the Internet. The symbol constant AF_UNIX is used for the former, and AF_INET for the latter (there are actually many other options which can be used here for specialized purposes).

The second argument is the type of socket. Recall that there are two choices here, a stream socket in which characters are read in a continuous stream as if from a file or pipe, and a datagram socket, in which messages are read in chunks. The two symbolic constants are SOCK_STREAM and SOCK_DGRAM.

The third argument is the protocol. If this argument is zero (and it always should be except for unusual circumstances), the operating system will choose the most appropriate protocol. It will choose TCP for stream sockets and UDP for datagram sockets.

The socket system call returns an entry into the file descriptor table (i.e. a small integer). This value is used for all subsequent references to this socket. If the socket call fails, it returns -1.

In this case the program displays and error message and exits. However, this system call is unlikely to fail.

This is a simplified description of the socket call; there are numerous other choices for domains and types, but these are the most common. The socket() man page has more information.

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

The function bzero() sets all values in a buffer to zero. It takes two arguments, the first is a pointer to the buffer and the second is the size of the buffer. Thus, this line initializes serv_addr to zeros. —-

portno = atoi(argv[1]);

The port number on which the server will listen for connections is passed in as an argument, and this statement uses the atoi() function to convert this from a string of digits to an integer.

serv_addr.sin_family = AF_INET;

The variable serv_addr is a structure of type struct sockaddr_in. This structure has four fields. The first field is short sin_family, which contains a code for the address family. It should always be set to the symbolic constant AF_INET.

serv_addr.sin_port = htons(portno);

The second field of serv_addr is unsigned short sin_port, which contain the port number. However, instead of simply copying the port number to this field, it is necessary to convert this to network byte order using the function htons() which converts a port number in host byte order to a port number in network byte order.

serv_addr.sin_addr.s_addr = INADDR_ANY;

The third field of sockaddr_in is a structure of type struct in_addr which contains only a single field unsigned long s_addr. This field contains the IP address of the host. For server code, this will always be the IP address of the machine on which the server is running, and there is a symbolic constant INADDR_ANY which gets this address.

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

The bind() system call binds a socket to an address, in this case the address of the current host and port number on which the server will run. It takes three arguments, the socket file descriptor, the address to which is bound, and the size of the address to which it is bound. The second argument is a pointer to a structure of type sockaddr, but what is passed in is a structure of type sockaddr_in, and so this must be cast to the correct type. This can fail for a number of reasons, the most obvious being that this socket is already in use on this machine. The bind() manual has more information.

listen(sockfd,5);

The listen system call allows the process to listen on the socket for connections. The first argument is the socket file descriptor, and the second is the size of the backlog queue, i.e., the number of connections that can be waiting while the process is handling a particular connection. This should be set to 5, the maximum size permitted by most systems. If the first argument is a valid socket, this call cannot fail, and so the code doesn’t check for errors. The listen() man page has more information.

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

The accept() system call causes the process to block until a client connects to the server. Thus, it wakes up the process when a connection from a client has been successfully established. It returns a new file descriptor, and all communication on this connection should be done using the new file descriptor. The second argument is a reference pointer to the address of the client on the other end of the connection, and the third argument is the size of this structure. The accept() man page has more information.

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

Note that we would only get to this point after a client has successfully connected to our server. This code initializes the buffer using the bzero() function, and then reads from the socket. Note that the read call uses the new file descriptor, the one returned by accept(), not the original file descriptor returned by socket(). Note also that the read() will block until there is something for it to read in the socket, i.e. after the client has executed a write().
It will read either the total number of characters in the socket or 255, whichever is less, and return the number of characters read. The read() man page has more information.

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

Once a connection has been established, both ends can both read and write to the connection. Naturally, everything written by the client will be read by the server, and everything written by the server will be read by the client. This code simply writes a short message to the client. The last argument of write is the size of the message. The write() man page has more information.

  return 0;
}

This terminates main and thus the program. Since main was declared to be of type int as specified by the ascii standard, some compilers complain if it does not return anything.

Client code

As before, we will go through the program client.c line by line.

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

The header files are the same as for the server with one addition. The file netdb.h defines the structure hostent, which will be used below.

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;

The error() function is identical to that in the server, as are the variables sockfd, portno, and n. The variable serv_addr will contain the address of the server to which we want to connect. It is of type struct sockaddr_in.
The variable server is a pointer to a structure of type hostent. This structure is defined in the header file netdb.h as follows:

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 */
};

It defines a host computer on the Internet. The members of this structure are:

  • 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.

Note that h_addr is an alias for the first address in the array of network addresses.

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");

All of this code is the same as that in the server.

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

The variable argv[1] contains the name of a host on the Internet, e.g. cs.rpi.edu. The function:

struct hostent *gethostbyname(char *name)

Takes such a name as an argument and returns a pointer to a hostent containing information about that host.
The field char *h_addr contains the IP address.

If this structure is NULL, the system could not locate a host with this name.

In the old days, this function worked by searching a system file called /etc/hosts but with the explosive growth of the Internet, it became impossible for system administrators to keep this file current. Thus, the mechanism by which this function works is complex, often involves querying large databases all around the country. The gethostbyname() man page has more information.

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);

This code sets the fields in serv_addr. Much of it is the same as in the server. However, because the field server->h_addr is a character string, we use the function:

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

which copies length bytes from s1 to s2. —-

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

The connect function is called by the client to establish a connection to the server. It takes three arguments, the socket file descriptor, the address of the host to which it wants to connect (including the port number), and the size of this address. This function returns 0 on success and -1 if it fails. The connect() man page has more information.
Notice that the client needs to know the port number of the server, but it does not need to know its own port number. This is typically assigned by the system when connect is called.

  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;
}

The remaining code should be fairly clear. It prompts the user to enter a message, uses fgets to read the message from stdin, writes the message to the socket, reads the reply from the socket, and displays this reply on the screen.

Socket服务端的设计

  • Designing servers

    There are a number of different ways to design servers. These models are discussed in detail in a book by Douglas E. Comer and David L. Stevens entiteld Internetworking with TCP/IP Volume III:Client Server Programming and Applications published by Prentice Hall in 1996. These are summarized here.

  • Concurrent, connection oriented servers

    The typical server in the Internet domain creates a stream socket and forks off a process to handle each new connection that it receives. This model is appropriate for services which will do a good deal of reading and writing over an extended period of time, such as a telnet server or an ftp server. This model has relatively high overhead, because forking off a new process is a time consuming operation, and because a stream socket which uses the TCP protocol has high kernel overhead, not only in establishing the connection but also in transmitting information. However, once the connection has been established, data transmission is reliable in both directions.

  • Iterative, connectionless servers

    Servers which provide only a single message to the client often do not involve forking, and often use a datagram socket rather than a stream socket. Examples include a finger daemon or a timeofday server or an echo server (a server which merely echoes a message sent by the client). These servers handle each message as it receives them in the same process. There is much less overhead with this type of server, but the communication is unreliable. A request or a reply may get lost in the Internet, and there is no built-in mechanism to detect and handle this.

  • Single Process concurrent servers

    A server which needs the capability of handling several clients simultaneous, but where each connection is I/O dominated (i.e. the server spends most of its time blocked waiting for a message from the client) is a candidate for a single process, concurrent server. In this model, one process maintains a number of open connections, and listens at each for a message. Whenever it gets a message from a client, it replies quickly and then listens for the next one. This type of service can be done with the select system call.