socket() 打开一个网络通讯端口,如果成功的话,就像 open() 一样返回一个文件描述符,应用程序可以像读写文件一样用 read/write 在网络上收发数据。对于IPv4,family 参数指定为 AF_INET。对于 TCP 协议,type 参数指定为 SOCK_STREAM,表示面向流的传输协议。如果是 UDP 协议,则 type 参数指定为 SOCK_DGRAM,表示面向数据报的传输协议。protocol 指定为 0 即可。
int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
服务器需要调用 bind 函数绑定一个固定的网络地址和端口号。bind() 的作用是将参数 sockfd 和 myaddr 绑定在一起,使 sockfd 这个用于网络通讯的文件描述符监听 myaddr 所描述的地址和端口号。struct sockaddr *是一个通用指针类型,myaddr 参数实际上可以接受多种协议的 sockaddr 结构体,而它们的长度各不相同,所以需要第三个参数 addrlen 指定结构体的长度。
程序中对 myaddr 参数的初始化为:
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
首先将整个结构体清零,然后设置地址类型为 AF_INET,网络地址为 INADDR_ANY,这个宏表示本地的任意 IP 地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个 IP 地址,这样设置可以在所有的 IP 地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个 IP 地址,端口号为 SERV_PORT,我们定义为 8000。
int listen(int sockfd, int backlog);
listen() 声明 sockfd 处于监听状态,并且最多允许有 backlog 个客户端处于连接待状态,如果接收到更多的连接请求就忽略。
int accept(int sockfd, struct sockaddr *cliaddr, socklen_t *addrlen);
三方握手完成后,服务器调用 accept() 接受连接,如果服务器调用 accept() 时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来。cliaddr 是一个传出参数,accept() 返回时传出客户端的地址和端口号。addrlen 参数是一个传入传出参数(value-result argument)���传入的是调用者提供的缓冲区 cliaddr 的长度以避免缓冲区溢出问题,传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。如果给 cliaddr 参数传 NULL,表示不关心客户端的地址。
服务器程序的主要结构如下:
while (1)
{
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd,
(struct sockaddr *)&cliaddr, &cliaddr_len);
n = read(connfd, buf, MAXLINE);
......
close(connfd);
}
整个是一个 while 死循环,每次循环处理一个客户端连接。由于 cliaddr_len 是传入传出参数,每次调用 accept( ) 之前应该重新赋初值。accept() 的参数 listenfd 是先前的监听文件描述符,而 accept() 的返回值是另外一个文件描述符 connfd,之后与客户端之间就通过这个 connfd 通讯,最后关闭 connfd 断开连接,而不关闭 listenfd,再次回到循环开头 listenfd 仍然用作 accept 的参数。
客户端程序下面是客户端程序,它从命令行参数中获得一个字符串发给服务器,然后接收服务器返回的字符串并打印:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXLINE 80
#define SERV_PORT 8000