Linux 网络编程
目录
TCP/IP 协议概述
OSI 参考模型及 TCP/IP 参考模型
TCP/IP 协议族
TCP 和 UDP
TCP
UDP
emm
地址结构相关处理
下面首先介绍两个重要的数据类型:sockaddr 和 sockaddr_in,这两个结构类型都是用来保存 socket 信息的,如下所示:
struct sockaddr
{
unsigned short sa_family; /*地址族*/
char sa_data[14]; /*14 字节的协议地址,包含该socket 的IP 地址和端口号。*/
};
struct sockaddr_in
{
short int sa_family; /*地址族*/
unsigned short int sin_port; /*端口号*/
struct in_addr sin_addr; /*IP 地址*/
unsigned char sin_zero[8]; /*填充 0 以保持与 struct sockaddr 同样大小*/
};
sa_family 常见字段
结构定义头文件 #include <netinet/in.h>
sa_family
- AF_INET:IPv4 协议
- AF_INET6:IPv6 协议
- AF_LOCAL:UNIX 域协议
- AF_LINK:链路地址协议
- AF_KEY:密钥套接字(socket)
数据存储优先顺序
计算机数据存储有两种字节优先顺序:高位字节优先(称为大端模式)和低位字节优先(称为小端模式,PC 机通常采用小端模式)。Internet 上数据以高位字节优先顺序在网络上传输,因此在有些情况下,需要对这两个字节存储优先顺序进行相互转化。这里用到了 4 个函数:htons()、ntohs()、htonl()和 ntohl()。这 4 个地址分别实现网络字节序和主机字节序的转化,这里的 h 代表 host,n 代表 network,s 代表 short,l 代表 long。通常 16 位的 IP 端口号用 s 代表,而 IP 地址用 l 来代表。
uint16_t htons(unit16_t host16bit) uint32_t htonl(unit32_t host32bit) uint16_t ntohs(unit16_t net16bit) uint32_t ntohs(unit32_t net32bit)
地址格式转化
通常用户在表达地址时采用的是点分十进制表示的数值(或者是以冒号分开的十进制 IPv6 地址),而在通常使用的 socket 编程中所使用的则是二进制值,这就需要将这两个数值进行转换。这里在 IPv4 中用到的函数有 inet_aton()、inet_addr()和 inet_ntoa(),而 IPv4 和 IPv6 兼容的函数有 inet_pton()和 inet_ntop()。由于 IPv6 是下一代互联网的标准协议,因此,本书讲解的函数都能够同时兼容 IPv4 和 IPv6,但在具体举例时仍以 IPv4 为例。
这里 inet_pton()函数是将点分十进制地址映射为二进制地址,而 inet_ntop()是将二进制地址映射为点分十进制地址
名字地址转化
通常,人们在使用过程中都不愿意记忆冗长的 IP 地址,尤其到 IPv6 时,地址长度多达 128 位,那时就更加不可能一次次记忆那么长的 IP 地址了。因此,使用主机名将会是很好的选择。在 Linux 中,同样有一些函数可以实现主机名和地址的转化,最为常见的有 gethostbyname()、gethostbyaddr()和 getaddrinfo()等,它们都可以实现 IPv4 和 IPv6 的地址和主机名之间的转化。其中 gethostbyname()是将主机名转化为 IP 地址,gethostbyaddr()则是逆操作,是将 IP 地址转化为主机名,另外 getaddrinfo()还能实现自动识别 IPv4 地址和 IPv6 地址
socket
socket() | 创建套接字
#include <sys/socket.h>
/**
* @brief 创建套接字
* @param family 协议族
* - AF_INET IPv4
* - AF_INET6 IPv6
* - AF_LOCAL UNIX 域协议
* - AF_ROUTE 路由套接字
* - AF_KEY 密钥套接字
* @param type 套接字类型
* - SOCK_STREAM 流式套接字
* - SOCK_DGRAM 数据报套接字
* - SOCK_RAW 原始套接字
* @param protocol 协议: 通常为 0(原始套接字除外)
* @return int 套接字描述符 | -1 失败
*/
int socket(int family, int type, int protocol);
bind() | 分配套接字地址
#include <sys/socket.h>
/**
* @brief 分配套接字地址
* @param sockfd 套接字描述符
* @param my_addr 本地地址结构
* @param addrlen 地址结构长度
* @return int 0 成功 | -1 失败
*/
int bind(int sockfd, const struct sockaddr *my_addr, socklen_t addrlen);
listen() | 监听连接
#include <sys/socket.h>
/**
* @brief 监听连接
* @param sockfd 套接字描述符
* @param backlog 请求队列中允许的最大请求数,大多数系统缺省值为 5
* @return int 0 成功 | -1 失败
*/
int listen(int sockfd, int backlog);
accept() | 接受连接
#include <sys/socket.h>
/**
* @brief 接受连接
* @param sockfd 套接字描述符
* @param addr 客户端地址
* @param addrlen 地址结构长度
* @return int 0 成功 | -1 失败
*/
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
connect() | 发起连接
#include <sys/socket.h>
/**
* @brief 发起连接
* @param sockfd 套接字描述符
* @param servaddr 指向数据结构 sockaddr 的指针
* @param addrlen 地址结构长度
* @return int 0 成功 | -1 失败
*/
int connect(int sockfd, const struct sockaddr *servaddr, socklen_t addrlen);
close() | 关闭连接
#include <unistd.h>
/**
* @brief 关闭连接
* @param sockfd 套接字描述符
* @return int 0 成功 | -1 失败
*/
int close(int sockfd);
send() | 发送数据
#include <sys/socket.h>
/**
* @brief 发送数据
* @param sockfd 套接字描述符
* @param msg 发送缓冲区
* @param len 发送数据长度
* @param flags 标志位
* @return int 成功发送的字节数 | -1 失败
*/
ssize_t send(int sockfd, const void *msg, size_t len, int flags);
recv() | 接收数据
#include <sys/socket.h>
/**
* @brief 接收数据
* @param sockfd 套接字描述符
* @param buf 接收缓冲区
* @param len 接收数据长度
* @param flags 标志位,通常为 0
* @return int 成功接收的字节数 | -1 失败
*/
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
sendto() | 发送数据
#include <sys/socket.h>
/**
* @brief 发送数据
* @param sockfd 套接字描述符
* @param msg 发送缓冲区
* @param len 发送数据长度
* @param flags 标志位
* @param to 目标地址
* @param tolen 地址结构长度
* @return int 成功发送的字节数 | -1 失败
*/
ssize_t sendto(int sockfd, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t tolen);
recvfrom() | 接收数据
#include <sys/socket.h>
/**
* @brief 接收数据
* @param sockfd 套接字描述符
* @param buf 接收缓冲区
* @param len 接收数据长度
* @param flags 标志位,通常为 0
* @param from 源地址
* @param fromlen 地址结构长度
* @return int 成功接收的字节数 | -1 失败
*/
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t *fromlen);
EG
该实例分为客户端和服务器端两部分,其中服务器端首先建立起 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];
/*建立 socket 连接*/
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(1);
}
printf("Socket id = %d\n", sockfd);
/*设置 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);
int i = 1; /* 允许重复使用本地地址与套接字进行绑定 */
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i));
/*绑定函数 bind()*/
if (bind(sockfd, (struct sockaddr *)&server_sockaddr,
sizeof(struct sockaddr)) == -1)
{
perror("bind");
exit(1);
}
printf("Bind success!\n");
/*调用 listen()函数,创建未处理请求的队列*/
if (listen(sockfd, MAX_QUE_CONN_NM) == -1)
{
perror("listen");
exit(1);
}
printf("Listening....\n");
/*调用 accept()函数,等待客户端的连接*/
if ((client_fd = accept(sockfd,
(struct sockaddr *)&client_sockaddr, &sin_size)) == -1)
{
perror("accept");
exit(1);
}
/*调用 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);
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);
}
/*地址解析函数*/
if ((host = gethostbyname(argv[1])) == NULL)
{
perror("gethostbyname");
exit(1);
}
memset(buf, 0, sizeof(buf));
sprintf(buf, "%s", argv[2]);
/*创建 socket*/
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket");
exit(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);
/*调用 connect 函数主动发起对服务器端的连接*/
if (connect(sockfd, (struct sockaddr *)&serv_addr,
sizeof(struct sockaddr)) == -1)
{
perror("connect");
exit(1);
}
/*发送消息给服务器端*/
if ((sendbytes = send(sockfd, buf, strlen(buf), 0)) == -1)
{
perror("send");
exit(1);
}
close(sockfd);
exit(0);
}
在运行时需要先启动服务器端,再启动客户端。
$ ./server
Socket id = 3
Bind success!
Listening....
Received a message: Hello,Server!
$ ./client localhost(或者输入 IP 地址) Hello,Server!
网络高级编程
在实际情况中,人们往往遇到多个客户端连接服务器端的情况。由于之前介绍的如 connet()、recv()和 send()等都是阻塞性函数,如果资源没有准备好,则调用该函数的进程将进入睡眠状态,这样就无法处理 I/O 多路复用的情况了。本节给出了两种解决 I/O 多路复用的解决方法,这两个函数都是之前学过的 fcntl()和 select()(请读者先复习第 6 章中的相关内容)。可以看到,由于在 Linux 中把 socket 也作为一种特殊文件描述符,这给用户的处理带来了很大的方便
fcntl() | 设置文件描述符状态标志
函数 fcntl()针对 socket 编程提供了如下的编程特性。
- 非阻塞 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_NONBLOCK。
- 异步 I/O:可将 cmd 设置为 F_SETFL,将 lock 设置为 O_ASYNC。
下面是用 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);
}
// $ ./net_fcntl
// Listening....
// accept: Resource temporarily unavailable
可以看到,当 accept()的资源不可用(没有任何未处理的等待连接的请求)时,程序就会自动返回。
select() | I/O 多路复用
使用 fcntl()函数虽然可以实现非阻塞 I/O 或信号驱动 I/O,但在实际使用时往往会对资源是否准备完毕进行循环测试,这样就大大增加了不必要的 CPU 资源的占用。在这里可以使用 select()函数来解决这个问题,同时,使用 select()函数还可以设置等待的时间,可以说功能更加强大。下面是使用 select()函数的服务器端源代码。客户端程序基本上与 10.2.3 小节中的例子相同,仅加入一行 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 while*/
close(sockfd);
exit(0);
}
运行该程序时,可以先启动服务器端,再反复运行客户端程序(这里启动两个客户端进程)即可,服务器端运行结果如下所示:
$ ./server
listening....
New connection from 4(socket) /* 接受第一个客户端的连接请求*/
Received a message from 4: Hello,First! /* 接收第一个客户端发送的数据*/
New connection from 5(socket) /* 接受第二个客户端的连接请求*/
Received a message from 5: Hello,Second! /* 接收第二个客户端发送的数据*/
Client 4(socket) has left /* 检测到第一个客户端离线了*/
Client 5(socket) has left /* 检测到第二个客户端离线了*/
$ ./client localhost Hello,First! & ./client localhost Hello,Second