前言
开坑作死小能手在csapp的大坑才刚开始的情况下又来开个新坑了。。。不想像csapp那坑那样作死了。。。此篇以代码为主。。。
UNIX网络编程分为两卷,分别为套接字联网API和进程间通信。
1.1 概述
编写网络通信程序前,需要确定程序通信所用的协议。大多数的网络应用是分为客户(Client)和服务器(Server)来组织的,通常称之为C/S结构。在书中使用一个头文件unp.h,其中包括了需要的头文件、一些错误处理函数以及包裹函数。
1.2 时间获取客户端
下面实现了一个查询当前时间的客户端程序。客户端与服务器建立一个TCP连接后,服务器返回当前的时间与日期。
#include "unp.h"
int main(int argc, char **argv) {
int sockfd, n;
char recvline[MAXLINE + 1];
struct sockaddr_in servaddr;
if (argc != 2) { // 使用argv[1]参数传递服务器端地址。
err_quit("Usage: daytimetcpcli <IPaddress>");
}
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { // 创建网际(AF_INET)字节流(SOCK_STREAM)套接字。
err_sys("socket error");
}
bzero(&servaddr, sizeof(servaddr)); // 使用bzero将servaddr清零,也可使用memset(),但memset带3个参数容易犯错。
servaddr.sin_family = AF_INET; // 将地址族置为AF_INET。
servaddr.sin_port = htons(13); // 服务器端端口号设为13。
if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) { // 将点分十进制的参数转换为合适的格式,并设置为服务器端的地址。
err_quit("inet_pton error for %s", argv[1]);
}
if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) < 0) { // 与指定的服务器端建立一个TCP连接。SA是一个宏定义,等同于struct sockaddr。
err_sys("connect error");
}
while ((n = read(sockfd, recvline, MAXLINE)) > 0) { // 读取服务器的应答,写入recvline中。当返回值为0(对端关闭连接)或为负(发送错误)时结束循环。
recvline[n] = 0;
if (fputs(recvline, stdout) == EOF) {
err_sys("fputs error");
}
}
if (n < 0) {
err_sys("read error");
}
exit(0);
}
在上述代码中用到的err_开头的函数即书中提供的错误处理函数,如err_quit("inet_pton error for %s", argv[1])
等同于
fprintf(stderr, "inet_pton error for %s", argv[1]);
exit(1);
1.3 时间获取服务器端
#include "unp.h"
#include <time.h>
int main(int argc, char **argv) {
int listenfd, connfd;
struct sockaddr_in servaddr;
char buff[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 将地址设为INADDR_ANY目的是若服务器主机有多个网络接口,服务器程序可以在任意一个接口上接受客户连接。
servaddr.sin_port = htons(13);
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr)); // 将端口捆绑到服务器进程的套接字。
Listen(listenfd, LISTENQ); // 将套接字转换为监听套接字,常量LISTENQ是允许这个监听描述符排队的最大客户连接数。
for ( ; ; ) {
connfd = Accept(listenfd, (SA *) NULL, NULL); // 接受客户连接
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, sizeof(buff)); // 发送应答
Close(connfd); // 关闭与客户的连接
}
}
在服务器端程序中使用了包裹函数,它将错误处理函数包含进其中,缩短了程序的长度。如Socket()
,它的实现如下
int Socket(int family, int type, int protocol) {
int n;
if ((n = socket(family, type, protocol)) < 0) {
err_sys("socket error");
}
return(n);
}
1.4 IPv6版本
上述的两个程序均为IPv4版本,通过稍微修改即可成为IPv6的版本。
在IPv4中,地址族为AF_INET,IPv6中则要修改为AF_INET6。除地址族外,将struct sockaddr_in
改为struct sockaddr_in6
,并将该结构变量中的sin_
开头的成员修改为sin6_
。除此外,在服务器程序中,需将servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
修改为servaddr.sin6_addr = in6addr_any
。
1.5 效果
编译客户端与服务器端后在命令行中进行测试,因为我的客户端与服务器端都在同一台电脑上,所以用了环回地址127.0.0.1以及IPv6的换回地址::1作为参数。先启动服务器端,服务器端开启时需要有root权限,下面分别为IPv4与IPv6的服务器端。
$sudo ./daytimetcpsrv
$sudu ./daytimetcpsrvv6
开启服务器端后即可使用客户端程序获取当前时间。
$./daytimetcpcli 127.0.0.1
Sat Oct 3 14:22:04 2015
$./daytimetcpcliv6 ::1
Sat Oct 3 14:23:38 2015