赞
踩
OSI 协议参考模型是基于国际标准化组织(ISO)的建议发展起来的,从上到下共分为 7 层:应用层、表示层、会话层、传输层、网络层、数据链路层及物理层。这个7 层的协议模型虽然规定得非常细致和完善,但在实际中却得不到广泛的应用,其重要的原因之一就在于它过于复杂。 但它仍是此后很多协议模型的基础,这种分层架构的思想在很多领域都得到了广泛的应用。
TCP/IP 协议模型从一开始就遵循简单明确的设计思路,它将TCP/IP 的7 层协议模型简化为4 层,从 而更有利于实现和使用。TCP/IP 的协议参考模型和OSI 协议参考模型的对应关系如图所示:
虽然 TCP/IP 名称只包含了两个协议,但实际上TCP/IP 是一个庞大的协议族,它包括了各个层次上的众多协议,下图列举了各层中一些重要的协议,并给出了各个协议在不同层次中所处的位置:
篇幅关系,在此主要介绍在网络编程中涉及的传输层 TCP 和 UDP 协议。
同其他任何协议栈一样,TCP 向相邻的高层提供服务。因为 TCP 的上一层就是应用层,因此,TCP 数据传输实 现了从一个应用程序到另一个应用程序的数据传递。应用程序通过编程调用 TCP 并使用 TCP 服务,提供需要准 备发送的数据,用来区分接收数据应用的目的地址和端口号。 通常应用程序通过打开一个 socket 来使用 TCP 服务,TCP 管理到其他 socket 的数据传递。可以说,通过 IP 的源/目的可以惟一地区分网络中两个设备的连接,通过 socket 的源/目的可以惟一地区分网络中两个应用程序的连接。
TCP 对话通过三次握手来进行初始化。三次握手的目的是使数据段的发送和接收同步,告诉其他主机其一 次可接收的数据量,并建立虚连接。 下面描述了这三次握手的简单过程。
TCP 实体所采用的基本协议是滑动窗口协议。当发送方传送一个数据报时,它将启动计时器。当该数据报 到达目的地后,接收方的 TCP 实体往回发送一个数据报,其中包含有一个确认序号,它表示希望收到的下 一个数据包的顺序号。如果发送方的定时器在确认信息到达之前超时,那么发送方会重发该数据包。
TCP 数据包头的格式如下:
UDP 即用户数据报协议,它是一种无连接协议,因此不需要像 TCP 那样通过三次握手来建立一个连接。 同时,一个 UDP 应用可同时作为应用的客户或服务器方。由于 UDP 协议并不需要建立一个明确的连接, 因此建立 UDP 应用要比建立 TCP 应用简单得多。
UDP 协议从问世至今已经被使用了很多年,虽然其最初的光彩已经被一些类似协议所掩盖,但是在网络质量越来越高的今天,UDP 的应用得到了大大的增强。它比 TCP 协议更为高效,也能更好地解决实时性的问题。如今,包括网络视频会议系统在内的众多的客户/服务器模式的网络应用都使用UDP 协议。
UDP数据报头的格式如下:
一个IP地址就是一个无符号32位整数。网络程序将其存放在如下所示结构体中:
struct in_addr{
uint32_t s_addr; //大端法表示的IP地址
};
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式, PC 机通常采用小端模式)。Internet 上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对 这两个字节存储优先顺序进行相互转化。
这里用到了 4 个函数:htons()、ntohs()、htonl()和 ntohl()。这 4 个地址分别实现网络字节序和主机字节序的转化,这里的 h 代表 host,n 代表 network,s 代表 short,l 代 表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表:
#include <netinet/in.h>
/* 成功返回要转换的字节序,失败返回-1 */
uint32_t htonl(uint32_t hostlong); //将主机字节序的32位hostlong转化为32位网络字节序
uint16_t htons(uint16_t hostshort); //将主机字节序的16位hostshort转化为16位网络字节序
uint32_t ntohl(uint32_t netlong); //将32位网络字节序的netlong转化为32位主机字节序
uint16_t ntohs(uint16_t netshort); //将16位网络字节序的netshort转化为16位主机字节序
注意:调用该函数只是使其得到相应的字节序,用户不需清楚该系统的主机字节序和网 络字节序是否真正相等。如果是相同不需要转换的话,该系统的这些函数会定义成空宏。
通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制 IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。
这里在 IPv4 中用到的函数有 inet_aton()、inet_addr()和 inet_ntoa(),而 IPv4 和 IPv6 兼容的函数有 inet_pton()和 inet_ntop()。由于 IPv6 是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容 IPv4 和 IPv6,但在具体举例时 仍以 IPv4 为例。 这里 inet_pton()函数是将点分十进制地址映射为二进制地址,而 inet_ntop()是将二进制地址映射为点分十进制地址。以上转化函数均位于头文件arpa/inet.h中。
in_addr_t inet_addr(const char *cp)
int inet_aton(const char *cp, struct in_addr *addr)
char *inet_ntoa(struct in_addr addr)
const char *inet_ntop(int family,const void *src, char *dst, socklen_t size)
int inet_pton(int family, const char *src, void *dst)
通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达 128 位,那时就更加 不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选择。
为了便于人类的记忆,DNS服务器记录了所有IP地址和主机名的映射,我们可以通过Linux系统下的nslookup
程序来查看域名对应的地址。默认本地主机域名localhost总是映射为回送地址(loopback address )127.0.0.1
$ nslookup localhost
Address:127.0.0.1
命令行下输入
hostname
会得到本机的IP地址。在通常情况下,多个域名可以映射到同一个或同一组IP地址;
此外,在 Linux 中,有一些函数可以实现主机名和地址的转化,最为常见的有 ==gethostbyname()、gethostbyaddr()、 getaddrinfo()、getnameinfo()==等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。以上接口函数均位于<netdb.h>中。
其中 gethostbyname()是将主机名转化为 IP 地址, gethostbyaddr()则是逆操作,是将 IP 地址转化为主机名。
gethostbyname()和 gethostbyaddr()都涉及一个 hostent 的结构体:
struct hostent
{
char *h_name; /*正式主机名*/
char **h_aliases; /*主机别名*/
int h_addrtype; /*地址类型*/
int h_length; /*地址字节长度*/
char **h_addr_list; /*指向 IPv4 或 IPv6 的地址指针数组*/
}
gethostbyname()
struct hostent *gethostbyname(const char *hostname)
另外,getaddrinfo()能实现自动识别 IPv4 地址和 IPv6 地址。它将主机名(网址或点分十进制IP地址)和服务名(端口号)的字符串转化成套接字地址结构。它是可重入和协议无关的,是代替gethostbyname和getservbyname函数的替代品。
getaddrinfo()函数涉及一个 addrinfo 的结构体:
struct addrinfo
{
int ai_flags; /*AI_PASSIVE, AI_CANONNAME;*/
int ai_family; /*地址族*/
int ai_socktype; /*socket 类型*/
int ai_protocol; /*协议类型*/
size_t ai_addrlen; /*地址字节长度*/
char *ai_canonname; /*主机名*/
struct sockaddr *ai_addr;/*socket 结构体*/
struct addrinfo *ai_next;/*下一个指针链表*/
}
getaddrinfo()
原型:int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **result)
参数:
host:可以是域名,也可以是点分十进制IP地址。
service :可以是服务名(http等),也可是十进制端口号。如果不想把主机名或端口号转换成套接字地址,就把相应的参数设置为NULL,但是至少要有一个有效。
hints :控制参数,它是一个特定的addrinfo指针,通过对该addrinfo结构体ai_family、ai_socktype、ai_protocol、ai_flags成员的设置,可以控制getaddrinfo函数返回的套接字地址列表的特性。但其余成员必须设置为0。实际中一般先用memset函数将整个结构清零,然后又选择的对某些成员赋值。
- ai_family: 可以设置为AF_INET或AF_INET6。前者表示只返回IPv4的地址,后者返回IPv6的地址;当设置为AF_UNSPEC时表示IPv4 或 IPv6 均可。
- ai_socktype :对host关联的每个地址,该函数默认最多返回3个addrinfo结构,每个的ai_socktype字段不同:“连接”、“数据报”和“原始套接字”。如果ai_socktype被设置为SOCK_STREAM,则表示为字节流套接字 (TCP),则将限制为对每个地址只返回1个连接的addrinfo结构。如果ai_socktype被设置为SOCK_DGRAM,则表示为数据报套接字 (UDP)。
- ai_flags : 它是一个位掩码,可以进一步修改默认行为,其取值可以是以下宏
- AI_ADDRCONFIG: 要求只有本地主机被配置为IPv4时,getaddrinfo返回IPv4地址。
- AI_CANONNAME: 将列表中第一个addrinfo结构的ai_canonname字段指向host的官方名字。
- AI_NUMERICSERV:强制getaddrinfo的service参数为端口号。
- AI_PASSIVE: getaddrinfo默认返回用于客户端的主动套接字,如果设置了该标志,那么函数会返回用于服务器的被动套接字,此时host设置为NULL。得到的套接字地址结构中的地址字段sa_data[14]是通配符地址(wildcard address),表示这个服务器会接受所有发送到服务器的IP地址的请求。
- ai_protocol:
- IPPROTO_IP:IP 协议
- IPPROTO_IPV4:IPv4 协议
- IPPROTO_IPV6:IPv6 协议
- IPPROTO_UDP:UDP
- IPPROTO_TCP:TCP
result : 指向一个包含sockaddr(套接字地址结构)地址的addrinfo结构体链表。一般调用完这个函数之后,会遍历该链表,依次尝试每个套接字地址,直到socket和connect或bind连接成功。
返回值:
备注:
通常服务器端在调用 getaddrinfo()之前,ai_flags 设置 AI_PASSIVE,用于 bind() 函数(用于端口和地址的绑定,后面会讲到),主机名 nodename 通常会设置为 NULL。
客户端调用 getaddrinfo()时,ai_flags 一般不设置 AI_PASSIVE,但是主机名 nodename 和服务名 servname(端口)则应该不为空。
即使不设置 ai_flags 为 AI_PASSIVE,取出的地址也可以被绑定,很多程序中 ai_flags 直接设置为 0,即 3 个标志位都不设置,这种情况下只要 hostname 和 servname 设置的没有问题就可以正确绑定。
为了避免内存泄漏,一般在调用完getaddrinfo函数之后,会调用freeaddrinfo函数释放该链表。其函数原型为void freeaddrinfo(struct addrinfo *result)
。
如果getaddrinfo遇到错误,应用程序可以调用gai_strerror函数将错误代码转换成字符串,其函数原型为const char *gai_strerror(int errcode)
。
当getaddrinfo创建列表中的addrinfo结构时,会填写除了ai_flags的每个字段。
getaddrinfo函数返回的addrinfo结构中的ai_addr指向的套接字地址可以直接用来传递给套接字接口中的函数(socket、connect、bind、listen、accept等),该特点使得我们编写的客户端和服务器能够独立于某个特殊版本的IP协议。下图展示了getaddrinfo返回的数据结构:
getnameinfo()
功能:将一个套接字地址结构转化成相应的主机名(网址或点分十进制IP地址)和服务名(端口号)字符串,并将它们复制到host和service缓冲区。它也是可重入和协议无关的,是代替gethostbyaddr和getservbyport函数的替代品。
原型:int getnameinfo( const struct sockaddr *sa, socklen_t salen, char *host, size_t hostlen, char *service, size_t servlen, int flags);
参数:
sa :指向大小为salen字节的套接字地址结构;
host :指向大小为hostlen字节的缓冲区;如果不想要主机名,可以设置为NULL,hostlen设置为0。
service :指向大小为servlen字节的缓冲区;如果不想要服务名,也可以设置为NULL,servlen设置为0,但二者不能同时都设为NULL。
flags :它是一个位掩码,可以进一步修改默认行为,其取值可以是以下宏的或。
- NI_NUMERICHOST: getnameinfo函数默认返回域名,若设置此项则返回数字地址。
- NI_NUMERICSERV: 默认返回服务名(如果可能的话),若设置此项则返回端口号。
#include <stdio.h>
#include <sys/inet.h>
#include "csapp.h"
int main(int argc, char **argv)
{
struct in_addr inaddr;
uint32_t addr;
char buf[MAXBUF];
if(argc != 2){
fprintf(stderr, "usage:%s <hex num>\n",argv[0]);
exit(0);
}
sscanf(argv[1],"%x",&addr);
inaddr.s_addr = htonl(addr);
if(!inet_ntop(AF_INET,&inaddr,buf,MAXBUF))
unix_error("inet_ntop");
printf("%s\n",buf);
exit(0);
}
编写函数将它的点分十进制串参数转换为十六进制:
#include <stdio.h>
#include <sys/inet.h>
#include "csapp.h"
int main(int argc, char **argv)
{
struct in_addr inaddr;
int rc;
if(argc != 2){
fprintf(stderr, "usage:%s <dotted-decimal>\n",argv[0]);
exit(0);
}
rc = inet_pton(AF_INET, argv[1], &inaddr);
if(rc == 0)
app_error("inet_pton error:invalid dotted-decimal address.\n");
elseif(rc < 0)
unix_error("inet_pton error.\n");
printf("0x%x\n",ntohl(inaddr.s_addr));
exit(0);
}
getaddrinfo 函数的用法
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
int main()
{
struct addrinfo hints, *res = NULL;
int rc;
memset(&hints, 0, sizeof(hints));
/*设置 addrinfo 结构体中各参数 */
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_DGRAM;
hints.ai_protocol = IPPROTO_UDP;
/*调用 getaddinfo 函数*/
rc = getaddrinfo("localhost", NULL, &hints, &res);
if (rc != 0)
{
perror("getaddrinfo");
exit(1);
}
else
{
printf("Host name is %s\n", res->ai_canonname);
}
exit(0);
}
当输入一个域名时,得到相应的点分十进制IPv4地址。
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
int main(int argc, char **argv)
{
struct addrinfo *p, *listp, hints;
char buf[MAXLINE];
int rc, flags;
if (argc != 2) {
fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
exit(0);
}
/* Get a list of addrinfo records */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_INET; /* IPv4 only */ //line:netp:hostinfo:family
hints.ai_socktype = SOCK_STREAM; /* Connections only */ //line:netp:hostinfo:socktype
if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
exit(1);
}
/* Walk the list and display each IP address */
flags = NI_NUMERICHOST; /* Display address string instead of domain name */
for (p = listp; p; p = p->ai_next) {
Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
printf("%s\n", buf);
}
/* Clean up */
Freeaddrinfo(listp);
exit(0);
}
首先,初始化hints结构,使getaddrinfo返回我们想要的地址。我们想得到用作“连接”的IPv4地址,且只想得到域名,不要服务名。
然后,遍历addrinfo结构链表,用getnameinfo将每个套接字地址转换成IPv4地址字符串。
最后,用freeaddrinfo函数释放链表。运行程序,我们会看到twitter.com映射到4个IP地址。
$ ./hostinfo twitter.com
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70
在 Linux 中的网络编程是通过 socket 接口来进行的。人们常说的 socket 是一种特殊的 I/O 接口,它也是一 种文件描述符。socket 是一种常用的进程之间通信机制,通过它不仅能实现本地机器上的进程之间的通信, 而且通过网络能够在不同机器上的进程之间进行通信。
每一个 socket 都用一个半相关描述{协议、本地地址、本地端口}来表示;一个完整的套接字则用一个相关 描述{协议、本地地址、本地端口、远程地址、远程端口}来表示。socket 也有一个类似于打开文件的函数 调用,该函数返回一个整型的 socket 描述符,随后的连接建立、数据传输等操作都是通过 socket 来实现的。
下图是基于套接口的网络应用概述:
从程序的角度看,套接字就是一个打开文件的描述符。套接字地址存放在以下两种类型的结构体中,其结构定义位于头文件 <netinet/in.h>中,其中sa_family指协议簇,主要包括以下几种:
sa_family:
/*IP套接字地址结构,_in是互联网的缩写*/
struct sockaddr_in{
uint16_t sin_family; //协议簇(AF_INET或AF_INET6)
uint16_t sin_port; //端口号
struct in_addr sin_addr; //IP地址
unsigned char sin_zero[8];//为了与struct sockaddr边界对齐而填充的0字节
};
/*通用套接字地址结构*/
struct sockaddr{
uint16_t sa_family; //协议簇(AF_INET或AF_INET6)
char sa_data[14];//地址数据
};
为什么会出现两种套接字地址?
网络编程函数connect、bind和accept要求一个指向与协议有关的套接字地址结构的指针。但套接字接口设计者面临的问题是如何定义这些函数,使之能够接受各种类型的套接字地址结构。而当时void *指针还没有发明,所以解决办法是设计的套接字函数都采用通用地址结构作为参数,而所有特定协议的套接字指针在使用时都强制转换成通用结构。
为了简化代码,Steven指导定义:
typedef struct sockaddr SA;
然后,无论何时需要将sockaddr_in结构强制转换成sockaddr结构时,我们都使用(SA)。
socket 编程的基本函数有 socket()、bind()、listen()、accept()、send()、sendto()、recv()以及 recvfrom()等,其 中根据客户端还是服务端,或者根据使用 TCP 协议还是 UDP 协议,这些函数的调用流程都有所区别,这里先对每个函数进行说明,再给出各种情况下使用的流程图。以下接口函数均位于头文件<sys/socket.h>中。
socket():该函数用于建立一个 socket 连接,可指定 socket 类型等信息。在建立了 socket 连接之后, 可对 sockaddr 或 sockaddr_in 结构进行初始化,以保存所建立的 socket 地址信息。
bind():该函数是用于将本地 IP 地址绑定到端口号,若绑定其他 IP 地址则不能成功。另外,它主要用于 TCP 的连接,而在 UDP 的连接中则无必要。
listen():在服务端程序成功建立套接字和与地址进行绑定之后,还需要准备在该套接字上接收新 的连接请求。此时调用 listen()函数来创建一个等待队列,在其中存放未处理的客户端连接请求。
accept():服务端程序调用 listen()函数创建等待队列之后,调用 accept()函数等待并接收客户端的 连接请求。它通常从由 bind()所创建的等待队列中取出第一个未处理的连接请求。
connect():该函数在 TCP 中是用于 bind()的之后的 client 端,用于与服务器端建立连接,而在 UDP 中由于没有了 bind()函数,因此用 connect()有点类似 bind()函数的作用。
send()和 recv():这两个函数分别用于发送和接收数据,可以用在 TCP 中,也可以用在 UDP 中。 当用在 UDP 时,可以在 connect()函数建立连接之后再用。
sendto()和 recvfrom():这两个函数的作用与 send()和 recv()函数类似,也可以用在 TCP 和 UDP 中。 当用在 TCP 时,后面的几个与地址有关参数不起作用,函数作用等同于 send()和 recv();当用在 UDP 时,可以用在之前没有使用 connect()的情况下,这两个函数可以自动寻找指定地址并进行连 接。
socket()
int socket(int family, int type, int protocol)
bind()
int bind(int sockfd, struct sockaddr *addr, socklen_t addrlen)
listen()
int listen(int sockfd, int backlog)
accept()
int accept(int listenfd, struct sockaddr *addr, int *addrlen)
connect()
int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen)
send()
int send(int sockfd, const void *msg, int len, int flags)
recv()
int recv(int sockfd, void *buf,int len, unsigned int flags)
sendto()
int sendto(int sockfd, const void *msg,int len, unsigned int flags, const struct sockaddr *to, int tolen)
recvfrom()
int recvfrom(int sockfd,void *buf, int len, unsigned int flags, struct sockaddr *from, int *fromlen)
使用TCP协议套接字编程流程图
使用UDP协议套接字编程流程图
编程要求:区分客户端和服务器端两部分,其中服务器端首先建立起 socket,然后与本地端口进行绑定,接着就开始接收从客户端的连接请求并建立与它的连接,并接收客户端发送的消息。客户端则在建立 socket 之后调用 connect()函数来建立连接。
服务端的代码如下所示:
/*server.c*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define BUFFER_SIZE 1024
#define MAX_QUE_CONN_NM 5
int main()
{
struct sockaddr_in server_sockaddr,client_sockaddr;
int sin_size,recvbytes;
int sockfd, client_fd;
char buf[BUFFER_SIZE];
/*1.建立 socket 连接*/
if ((sockfd = socket(AF_INET,SOCK_STREAM,0))== -1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n",sockfd);
/*1.1设置 sockaddr_in 结构体中相关参数*/
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_sockaddr.sin_zero), 8);
/*1.2允许重复使用本地地址与套接字进行绑定 */
int i = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
/*2.绑定函数 bind()*/
if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
/*3.调用 listen()函数,创建未处理请求的队列*/
if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("Listening....\n");
/*4.调用 accept()函数,等待客户端的连接*/
if ((client_fd = accept(sockfd, (struct sockaddr *)&client_sockaddr, &sin_size)) == -1)
{
perror("accept");
exit(1);
}
/*5.调用 recv()函数接收客户端的请求*/
memset(buf , 0, sizeof(buf));
if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) == -1)
{
perror("recv");
exit(1);
}
printf("Received a message: %s\n", buf);
/*6.关闭套接字*/
close(sockfd);
exit(0);
}
客户端的代码如下所示:
/*client.c*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 4321
#define BUFFER_SIZE 1024
int main(int argc, char *argv[])
{
int sockfd,sendbytes;
char buf[BUFFER_SIZE];
struct hostent *host;
struct sockaddr_in serv_addr;
if(argc < 3)
{
fprintf(stderr,"USAGE: ./client Hostname(or ip address) Text\n");
exit(1);
}
/*1.地址解析函数*/
if ((host = gethostbyname(argv[1])) == NULL)
{
perror("gethostbyname");
exit(1);
}
memset(buf, 0, sizeof(buf));
sprintf(buf, "%s", argv[2]);
/*2.创建 socket*/
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
/*2.1设置 sockaddr_in 结构体中相关参数*/
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_addr = *((struct in_addr *)host->h_addr);
bzero(&(serv_addr.sin_zero), 8);
/*3.调用 connect 函数主动发起对服务器端的连接*/
if(connect(sockfd,(struct sockaddr *)&serv_addr,
sizeof(struct sockaddr))== -1)
{
perror("connect");
exit(1);
}
/*4.发送消息给服务器端*/
if ((sendbytes = send(sockfd, buf, strlen(buf), 0)) == -1)
{
perror("send");
exit(1);
}
/*5.关闭套接字*/
close(sockfd);
exit(0);
}
运行结果:运行时需要先启动服务器端,再启动客户端!
套接字并发编程
当遇到多个客户端连接服务器端的情况时,由于之前介绍的如 connet()、recv()和 send() 等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多 路复用的情况了。下面给出两种解决 I/O 多路复用的解决方法,一是利用 fcntl(),二是利用select()函数。
fcntl():针对 socket 编程提供了2种编程特性。
下面是用 fcntl()将套接字设置为非阻塞 I/O 的实例代码:
/* net_fcntl.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#include <fcntl.h>
#define PORT 1234
#define MAX_QUE_CONN_NM 5
#define BUFFER_SIZE 1024
int main()
{
struct sockaddr_in server_sockaddr, client_sockaddr;
int sin_size, recvbytes, flags;
int sockfd, client_fd;
char buf[BUFFER_SIZE];
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_sockaddr.sin_zero), 8);
/* 允许重复使用本地地址与套接字进行绑定 */
int i = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
if(listen(sockfd,MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("Listening....\n");
/* 调用 fcntl()函数给套接字设置非阻塞属性 */
flags = fcntl(sockfd, F_GETFL);
if (flags < 0 || fcntl(sockfd, F_SETFL, flags|O_NONBLOCK) < 0)
{
perror("fcntl");
exit(1);
}
while(1)
{
sin_size = sizeof(struct sockaddr_in);
if ((client_fd = accept(sockfd, (struct sockaddr*)&client_sockaddr, &sin_size)) < 0)
{
perror("accept");
exit(1);
}
if ((recvbytes = recv(client_fd, buf, BUFFER_SIZE, 0)) < 0)
{
perror("recv");
exit(1);
}
printf("Received a message: %s\n", buf);
} /*while*/
close(client_fd);
exit(1);
}
运行该程序,结果可以看到,当 accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回:
$ ./net_fcntl
Listening....
accept: Resource temporarily unavailable
select():使用 fcntl()函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源的占用。在这里可以使用 select()函数来解决这个问题, 同时,使用 select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用 select()函数的服务器端 源代码。客户端程序基本上与 之前例子相同,仅加入一行 sleep()函数,使得客户端进程等待几秒钟才结束。
/* net_select.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
#define PORT 4321
#define MAX_QUE_CONN_NM 5
#define MAX_SOCK_FD FD_SETSIZE
#define BUFFER_SIZE 1024
int main()
{
struct sockaddr_in server_sockaddr, client_sockaddr;
int sin_size, count;
fd_set inset, tmp_inset;
int sockfd, client_fd, fd;
char buf[BUFFER_SIZE];
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
server_sockaddr.sin_family = AF_INET;
server_sockaddr.sin_port = htons(PORT);
server_sockaddr.sin_addr.s_addr = INADDR_ANY;
bzero(&(server_sockaddr.sin_zero), 8);
int i = 1;/* 允许重复使用本地地址与套接字进行绑定 */
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
if (bind(sockfd, (struct sockaddr *)&server_sockaddr, sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
if(listen(sockfd, MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("listening....\n");
/*将调用 socket()函数的描述符作为文件描述符*/
FD_ZERO(&inset);
FD_SET(sockfd, &inset);
while(1)
{
tmp_inset = inset;
sin_size = sizeof(struct sockaddr_in);
memset(buf, 0, sizeof(buf));
/*调用 select()函数*/
if (!(select(MAX_SOCK_FD, &tmp_inset, NULL, NULL, NULL) > 0))
{
perror("select");
}
for (fd = 0; fd < MAX_SOCK_FD; fd++)
{
if (FD_ISSET(fd, &tmp_inset) > 0)
{
if (fd == sockfd)
{ /* 服务端接收客户端的连接请求 */
if ((client_fd = accept(sockfd, (struct sockaddr *)&client_sockaddr, &sin_size))== -1)
{
perror("accept");
exit(1);
}
FD_SET(client_fd, &inset);
printf("New connection from %d(socket)\n", client_fd);
}
else /* 处理从客户端发来的消息 */
{
if ((count = recv(client_fd, buf, BUFFER_SIZE, 0)) > 0)
{
printf("Received a message from %d: %s\n", client_fd, buf);
}
else
{
close(fd);
FD_CLR(fd, &inset);
printf("Client %d(socket) has left\n", fd);
}
}
} /* end of if FD_ISSET*/
} /* end of for fd*/
} /* end if while */
close(sockfd);
exit(0);
}
运行该程序时,可以先启动服务器端,再反复运行客户端程序(这里启动两个客户端进程)即可,服务器端运行结果如下所示:
套接字接口函数和转换函数看上去有些可怕,相当复杂凌乱。这一节会介绍一些包装函数,它们将会大大简化客户端和服务器通信程序的编写。
客户端可以直接利用该函数建立与服务器的连接。
#include "csapp.h"
int open_clientfd(char * hostname, char *port);
//成功返回套接字描述符,出错返回-1
下面是它的源代码,它是可重入和协议无关的。
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;
}
假设服务器运行在主机hostname上,并在端口port上监听连接请求。
首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和connect成功。如果一个失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功建立连接,就释放列表内存,并把套接字描述符返回给客户端,客户端就可以利用它与所有Unix I/O函数与服务器通信了。
服务器可以直接利用该函数创建一个监听描述符,准备好建立与客户端的连接。
#include "csapp.h"
int open_listenfd(char *port);
//成功返回套接字描述符,出错返回-1
open_listenfd函数返回一个打开的监听描述符,且已经准备好在端口port上接受客户端的连接请求。下面是它的源代码:
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;
}
首先,调用getaddrinfo,返回一个addrinfo结构体链表。遍历该列表依次尝试列表中的每个条目中的ai_addr指向的套接字地址,直到调用socket和bind成功。如果失败,则在下一次尝试前关闭掉这个套接字描述符。如果成功绑定,就释放列表内存,并调用listen函数将该套接字描述符转换为监听描述符返回给调用者,服务器就可以利用它与所有Unix I/O函数响应客户端了。
- 我们使用了setsockopt函数来配置服务器,使得服务器能够被终止、重启和立即接受连接。一个重启的服务器默认将在30秒内拒绝客户端的连接请求。关于setsockopt的使用很复杂,将会专门写篇文章来讲解他的使用方法。
- 我们使用了AI_PASSIVE标志并将host参数设置为NULL,这样每个套接字地址字段都会被设置为通配符地址,表示服务器接受发送到本机所有IP地址的请求。
客户端首先与服务器建立连接,之后进入循环等待从标准输入读取文本行发送给服务器。再等待从服务器取回回送的行,并输出结果到标准输出。
#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);
}
服务器首先打开监听描述符,进入循环等待与客户端建立连接,连接之后首先输出客户端的域名和IP,之后调用echo函数为其服务。echo函数返回后关闭已连接的描述符,连接终止。
#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);
}
}
获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。