有了前面提到的概念,就可以开始根据客户-服务器模型,逐步进行网络连接程序的编写了。利用C语言编写的网络程序,大致流程如下图所示:
对于客户(左侧的Client)而言,首先需要获取自身的地址信息(getaddrinfo),随后利用这个信息创建一个socket地址(socket),接下来就进入了连接服务器的阶段(connect);而对于服务器(右侧的Server)而言,一开始也是获取自身的地址信息(getaddrinfo)以及创建一个socket地址(socket),随后便将socket地址与socket的descriptor相关联(bind),然后再声明这个descriptor是用于服务器而不是客户的(listen),最后等待客户发来的请求并接受(accept)
接下来,对这些方法进行一个简单的介绍:
网络连接的基本函数
基本结构
sockaddr
用于表示socket地址的结构:
struct sockaddr{
uint16_t sa_family; /*协议*/
char sa_data[14]; /*地址相关信息*/
};
sockaddr_in
用于表示IP socket地址的结构:
struct sockaddr_in{
uint16_t sin_family; /*协议*/
uint16_t sin_port; /*端口号*/
struct in_addr sin_addr; /*IP地址*/
unsigned char sin_zero[8];
};
sockaddr是sockaddr_in的普遍形式,在void *还没有出现的时候,就是用的sockaddr来避免非sockaddr_in导致的数据类型不一致问题
socket方法
socket方法会根据输入的domain和类型选项,来打开一个socket并返回对应的descriptor:
int socket(int domain, int type, int protocol);
当domain指定为AF_INET时,说明使用的是32位IP;而当type指定为SOCK_STREAM时,则说明这个socket将会被指定为连接的终点
connect方法
指定要连接的服务器的socket地址,就可以在自身的client与服务器之间使用connect建立连接:
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);
其中,addr是关于服务器的socket地址,而addrlen则是sizeof(sockaddr_in)的值,表示数据类型sockaddr_in的大小
在连接成功或出错前,此方法会一直阻塞
bind方法
此方法会对server中的socket地址和服务器的socket descriptor进行绑定:
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
同之前提到的connect方法,addr是关于服务器的socket地址,addrlen则是sizeof(sockaddr_in)的值,表示数据类型sockaddr_in的大小
listen方法
此方法会声明某个socket descriptor所对应的socket,是服务器而非客户的socket:
int listen(int sockfd, int backlog);
参数backlog一般会设定为1024,与协议有关
accept方法
使用accept方法,可以与客户相连接:
int accept(int listenfd, struct sockaddr *addr, int *addrlen);
其中,listenfd是在listen中传入的sockfd,这个socket已经被指定为了是服务器的socket;而addr则是用于存储客户的socket地址信息,在连接成功后,addr会被填充为客户的socket地址
host与服务(service)的相互转换
host基本信息可以被转换为socket地址结构,而反过来socket地址结构也可以被转换回host基本信息。这样的转换可以使用getaddrinfo和getnameinfo这两个方法来实现
getaddrinfo方法
此方法会将host名、host地址、服务名和端口号转换为socket地址的结构:
int getaddrinfo(const char *host, const char *service, const struct addrinfo *hints, struct addrinfo **result);
其中,host和service为socket地址的两个成分,而hint则用于指定一些条件。
hint所属的addrinfo结构定义如下所示:
struct addrinfo{
int ai_flags;
int ai_family; /*socket方法中的第一个参数*/
int ai_socktype; /*socket方法中的第二个参数*/
int ai_protocol; /*socket方法中的第三个参数*/
char *ai_canonname;
size_t ai_addrlen;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
};
在使用hint指定条件的时候,只设置ai_family、ai_socktype、ai_protocol和ai_flags这四个参数。其中前三个已经在前面提到过,而最后一个ai_flags则可以进行一些设置,例如设定为AI_NUMERICSERV时表示service参数为一个端口数字编号
在使用了此方法后,result将会指向一个由多个addrinfo结构连接在一起的链表,遍历这个链表即可得到所有与输入相对应的地址信息:
getnameinfo方法
getnameinfo方法与getaddrinfo方法相反,是将socket结构转换回来的方法:
int getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
sa是待转换的socket地址结构,而host和service则是准备接收到的转换后的信息,与在getaddrinfo中输入的host和service相对应
下述程序使用了getaddrinfo和getnameinfo方法,实现了从域名到ip地址的转换的功能:
#include<sys/types.h>
#include<sys/socket.h>
#include<netdb.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#define MAXLINE 8192
int main(int argc, char **argv){
struct addrinfo hints;
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
struct addrinfo *result;
if(getaddrinfo(argv[1], NULL, &hints, &result) != 0){
printf("Error in getaddrinfo!\n");
exit(0);
}
printf("ohhhhh\n");
int flags = NI_NUMERICHOST;
struct addrinfo *p;
p = result;
char host[MAXLINE];
while(p != NULL){
getnameinfo(p->ai_addr, p->ai_addrlen, host, MAXLINE, NULL, 0, flags);
printf("%s\n", host);
p = p->ai_next;
}
freeaddrinfo(result);
return 0;
}
输入:
./a.out www.baidu.com
得到输出结果:
ohhhhh
14.215.177.38
14.215.177.39
程序实现
使用前面所提到的那些方法,就可以实现简易的客户host和服务器host了
接下来将对csapp官方文档中的几个相关程序进行分析,以较为全面地理解网络编程的完整流程
从客户端连接到服务端:open_clientfd
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0) { /* Connect failed, try another */ //line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
官方提供的open_clientfd方法,输入的参数有两个,分别为服务端的hostname(地址)和port(端口)。而输出的参数则是成功连接到服务端的那个客户端,所对应的descriptor值
在此方法中,一开始使用了getaddrinfo,试图获取服务端的地址(line 10)。随后对返回的链表中的所有地址,都尝试创建一个socket descriptor(line 18)。创建成功后,便尝试将此descriptor和服务器地址进行connect(line 22)。最后,如果连接成功,则返回descriptor值,作为客户端的socket descriptor
在服务端创建socket并监听:open_listenfd
在服务端,使用下述代码,等待从客户端传来的消息并进行响应:
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int));
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0) { /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
open_listenfd方法输入的参数为端口号,这将作为此服务器的端口
在此方法中,首先也用了getaddrinfo获取了服务端的socket地址(line 11),随后创建socket descriptor(line 19),再将此descriptor绑定在服务器地址上(line 27),绑定成功后,使用listen(line 42)将其设定为服务器对应的socket,最后返回socket descriptor
在客户端向服务器发送消息并接收
#include "csapp.h"
int main(int argc, char **argv)
{
int clientfd;
char *host, *port, buf[MAXLINE];
rio_t rio;
if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(0);
}
host = argv[1];
port = argv[2];
clientfd = Open_clientfd(host, port);
Rio_readinitb(&rio, clientfd);
while (Fgets(buf, MAXLINE, stdin) != NULL) {
Rio_writen(clientfd, buf, strlen(buf));
Rio_readlineb(&rio, buf, MAXLINE);
Fputs(buf, stdout);
}
Close(clientfd); //line:netp:echoclient:close
exit(0);
}
从代码中可以看出,在使用open_clientfd打开客户socket对应的descriptor后,就开始尝试获取用户的输入并向服务器发送消息了。这里获取输入的方式是循环往复的,且在获取输入后会先向socket发送消息,随后再从socket对应的缓存中读取接收到的消息并打印在屏幕(标准输出)上。而如果退出循环,则会在释放descriptor后,再退出程序
在服务端接收消息并作出回复
#include "csapp.h"
void echo(int connfd);
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; /* Enough space for any address */ //line:netp:echoserveri:sockaddrstorage
char client_hostname[MAXLINE], client_port[MAXLINE];
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(0);
}
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(struct sockaddr_storage);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}
exit(0);
}
void echo(int connfd)
{
size_t n;
char buf[MAXLINE];
rio_t rio;
Rio_readinitb(&rio, connfd);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) { //line:netp:echo:eof
printf("server received %d bytes\n", (int)n);
Rio_writen(connfd, buf, n);
}
}
服务端在使用open_listenfd创建socket descriptor后,便会进入一个循环。在这个循环中,服务端会不断地等待客户端发来的消息,一旦accept成功,则会将客户端发来的消息打印出来
运行效果
在一个窗口中运行服务端,将端口设置为20,随后再在另一个窗口中运行客户端,地址设置为127.0.0.1(本地localhost对应的ip),端口也设置为20。运行后即可发现,连接成功:
随后在客户端输入消息,即可观察到服务端能够显示出接收到多少字节,同时将消息发回给客户端并在客户端显示传回的消息:
这样一来,最基本的服务器就算是搭建好了