网络通信
之前有写过socket网络编程的一些基本概念,最近对一些知识点进行梳理之后打算详细的、有层次地归纳一下这些基础概念。
一、基本概念
1. ISO/OSI七层网络协议模型
2. TCP/IP协议族
- TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
- UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
- IP (Internet Protocol, 互联网协议) 信息传递机制。
3. TCP/IP协议与ISO/OSI模型的对比
4. 消息流
5. 消息包
6.IP地址
- IP地址是Internet中唯一的地址标识
A. 一个IP地址占32位,正在扩充至128位。
B. 每个Internet包必须带IP地址。 - 点分十进制表示法
0x01020304 -> 1.2.3.4,高数位在左,低数位在右。 - IP地址分级
A级:0 + 7位网络地址 + 24位本地地址
B级:10 + 14位网络地址 + 16位本地地址
C级:110 + 21位网络地址 + 8位本地地址
D级:1110 + 28位多播(Muticast)地址 - 子网掩码
IP地址 & 子网掩码 = 网络地址
IP地址: 192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182
本地地址:48
二、套接字(Socket)
1. 接口
2. 模式
- 点对点(Peer-to-Peer, P2P):一对一的通信。
- 客户机/服务器(Client/Server, C/S):一对多的通信。
3. 绑定
先要有一个套接字描述符,还要有物理通信载体,然后将二者绑定在一起。
4. 函数
-
创建套接字
套接字描述符类似于文件描述符,UNIX把网络当文件看待,
发送数据即写文件,接收数据即读文件,一切皆文件。#include <sys/socket.h> int socket (int domain, int type, int protocol); domain - 域/地址族,取值: AF_UNIX/AF_LOCAL/AF_FILE: 本地通信(进程间通信); AF_INET: 基于TCP/IPv4(32位IP地址)的网络通信; AF_INET6: 基于TCP/IPv6(128位IP地址)的网络通信; AF_PACKET: 基于底层包接口的网络通信。 type - 通信协议,取值: SOCK_STREAM: 数据流协议,即TCP协议; SOCK_DGRAM: 数据报协议,即UDP协议。 protocol - 特别通信协议,一般不用,置0即可。 成功返回套接字描述符,失败返回-1。
-
准备通信地址
IP地址用于定位主机,端口号用于定位主机上的进程。 A. 基本地址类型 struct sockaddr { sa_family_t sa_family; // 地址族 char sa_data[14]; // 地址值 }; B. 本地地址类型 #include <sys/un.h> struct sockaddr_un { sa_family_t sun_family; // 地址族 char sun_path[]; // 套接字文件路径 }; C. 网络地址类型 #include <netinet/in.h> struct sockaddr_in { // 地址族 sa_family_t sin_family; // 端口号 // unsigned short, 0-65535 // 逻辑上表示一个参与通信的进程 // 使用时需要转成网络字节序 // 0-1024端口一般被系统占用 // 如:21-FTP、23-Telnet、80-WWW in_port_t sin_port; // IP地址 struct in_addr sin_addr; }; struct in_addr { in_addr_t s_addr; }; typedef uint32_t in_addr_t;
-
将套接字和通信地址绑定在一起
#include <sys/socket.h> int bind (int sockfd, const struct sockaddr* addr,socklen_t addrlen); 成功返回0,失败返回-1。
-
建立连接
#include <sys/socket.h> int connect (int sockfd, const struct sockaddr* addr,socklen_t addrlen); 成功返回0,失败返回-1。
-
用读写文件的方式通信:read/write
-
关闭套接字:close
-
字节序转换
#include <arpa/inet.h> // 32位无符号整数,主机字节序 -> 网络字节序 uint32_t htonl (uint32_t hostlong); // 16位无符号整数,主机字节序 -> 网络字节序 uint16_t htons (uint16_t hostshort); // 32位无符号整数,网络字节序 -> 主机字节序 uint32_t ntohl (uint32_t netlong); // 16位无符号整数,网络字节序 -> 主机字节序 uint16_t ntohs (uint16_t netshort);
-
主机字节序因处理器架构而异,有的采用小端字节序,有的采用大端字节序。网络字节序则固定采用大端字节序。
-
IP地址转换
#include <arpa/inet.h> // 点分十进制字符串 -> 网络字节序32位无符号整数 in_addr_t inet_addr (const char* cp); // 点分十进制字符串 -> 网络字节序32位无符号整数 int inet_aton (const char* cp, struct in_addr* inp); // 网络字节序32位无符号整数 -> 点分十进制字符串 char* inet_ntoa (struct in_addr in);
5. 编程
- 本地通信
服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->发送数据->关闭套接字
范例:locsvr.c、loccli.c
locsvr.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "/tmp/sock"
int main ()
{
printf ("服务器:创建本地数据报套接字...\n");
int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy (addr.sun_path, SOCK_FILE);
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("bind");
return -1;
}
printf ("服务器:接收数据...\n");
for (;;)
{
char buf[1024];
ssize_t rb = read (sockfd, buf, sizeof (buf));
if (rb == -1)
{
perror ("read");
return -1;
}
if (! strcmp (buf, "!!"))
break;
printf ("< %s\n", buf);
}
printf ("服务器:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("服务器:删除套接字文件...\n");
if (unlink (SOCK_FILE) == -1)
{
perror ("unlink");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
loccli.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "/tmp/sock"
int main ()
{
printf ("客户机:创建本地数据报套接字...\n");
int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_un addr;
addr.sun_family = AF_LOCAL;
strcpy (addr.sun_path, SOCK_FILE);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("connect");
return -1;
}
printf ("客户机:发送数据...\n");
for (;;)
{
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (write (sockfd, buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1)
{
perror ("write");
return -1;
}
if (! strcmp (buf, "!!"))
break;
}
printf ("客户机:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("客户机:大功告成!\n");
return 0;
}
- 网络通信
服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发送数据->关闭套接字
范例:netsvr.c、netcli.c
netsvr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main (int argc, char* argv[])
{
if (argc < 2)
{
fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
printf ("服务器:创建网络数据报套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("bind");
return -1;
}
printf ("服务器:接收数据...\n");
for (;;)
{
char buf[1024];
ssize_t rb = read (sockfd, buf, sizeof (buf));
if (rb == -1)
{
perror ("read");
return -1;
}
if (! strcmp (buf, "!!"))
break;
printf ("< %s\n", buf);
}
printf ("服务器:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
netcli.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 3)
{
fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
return -1;
}
printf ("客户机:创建网络数据报套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("connect");
return -1;
}
printf ("客户机:发送数据...\n");
for (;;)
{
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (write (sockfd, buf, (strlen (buf) + 1) *
sizeof (buf[0])) == -1)
{
perror ("write");
return -1;
}
if (! strcmp (buf, "!!"))
break;
}
printf ("客户机:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("客户机:大功告成!\n");
return 0;
}
三、基于TCP协议的客户机/服务器模型
1. 基本特征
-
面向连接。
-
可靠,保证数据的完整性和有序性。
A B C D E F
每个发送都有应答,若在时间窗口内没有收到A的应答,则从A开始重发。
2. 编程模型
3. 常用函数
-
listen
#include <sys/socket.h> int listen (int sockfd, int backlog); 将sockfd参数所标识的套接字标记为被动模式,使之可用于接受连接请求。 backlog参数表示未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。 若服务器端的未决连接数已达此限,则客户机端的connect()函数将返回-1,且errno为ECONNREFUSED。 成功返回0,失败返回-1。
2. accept
int accept (int sockfd, struct sockaddr* addr,socklen_t* addrlen);
从sockfd参数所标识套接字的未决连接请求队列中,提取第一个连接请求。
同时创建一个新的套接字,用于在该连接中通信,返回该套接字的描述符。
addr和addrlen参数用于输出连接请求发起者的地址信息。
成功返回通信套接字描述符,失败返回-1。
3. recv
ssize_t recv (int sockfd, void* buf, size_t len,int flags);
通过sockfd参数所标识的套接字,
期望接收len个字节到buf所指向的缓冲区中。
成功返回实际接收到的字节数,失败返回-1。
-
send
ssize_t send (int sockfd, const void* buf,size_t len, int flags); 通过sockfd参数所标识的套接字, 从buf所指向的缓冲区中发送len个字节。 成功返回实际被发送的字节数,失败返回-1。
范例:tcpsvr.c、tcpcli.c
tcpsvr.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void sigchld (int signum)
{
for (;;)
{
pid_t pid = waitpid (-1, 0, WNOHANG);
if (pid == -1)
{
if (errno != ECHILD)
{
perror ("waitpid");
exit (-1);
}
printf ("服务器:全部子进程都已退出。\n");
break;
}
if (pid)
printf ("服务器:发现%u子进程退出。\n", pid);
else
{
printf ("服务器:暂时没有子进程退出。\n");
break;
}
}
}
int main (int argc, char* argv[])
{
if (argc < 2)
{
fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
if (signal (SIGCHLD, sigchld) == SIG_ERR)
{
perror ("signal");
return -1;
}
printf ("服务器:创建网络数据流套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("bind");
return -1;
}
printf ("服务器:监听套接字...\n");
if (listen (sockfd, 1024) == -1)
{
perror ("listen");
return -1;
}
for (;;)
{
printf ("服务器:等待连接请求...\n");
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
int connfd = accept (sockfd, (struct sockaddr*)&addrcli,&addrlen);
if (connfd == -1)
{
perror ("accept");
return -1;
}
printf ("服务器:接受来自%s:%u客户机的连接请求。"
"创建子进程为其提供服务...\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
pid_t pid = fork ();
if (pid == -1)
{
perror ("fork");
return -1;
}
if (pid == 0)
{
printf ("%u子进程:为%s:%u客户机提供服务...\n", getpid (),
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
for (;;)
{
char buf[1024];
printf ("%u子进程:接收请求...\n", getpid ());
ssize_t rb = recv (connfd, buf, sizeof (buf), 0);
if (rb == -1)
{
perror ("recv");
return -1;
}
if (rb == 0)
{
printf ("%u子进程:客户机已关闭连接。\n",
getpid ());
break;
}
printf ("%u子进程:发送响应...\n", getpid ());
if (send (connfd, buf, rb, 0) == -1)
{
perror ("send");
return -1;
}
}
printf ("%u子进程:关闭连接套接字...\n", getpid ());
if (close (connfd) == -1)
{
perror ("close");
return -1;
}
printf ("%u子进程:即将退出。\n", getpid ());
return 0;
}
if (close (connfd) == -1)
{
perror ("close");
return -1;
}
}
printf ("服务器:关闭监听套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
tcpcli.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 3)
{
fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
return -1;
}
printf ("客户机:创建网络数据流套接字...\n");
int sockfd = socket (AF_INET, SOCK_STREAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("客户机:准备地址并连接...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
if (connect (sockfd, (struct sockaddr*)&addr,
sizeof (addr)) == -1)
{
perror ("connect");
return -1;
}
printf ("客户机:发送请求并接收响应...\n");
for (;;)
{
printf ("> ");
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (send (sockfd, buf, (strlen (buf) + 1) *
sizeof (buf[0]), 0) == -1)
{
perror ("send");
return -1;
}
ssize_t rb = recv (sockfd, buf, sizeof (buf), 0);
if (rb == -1)
{
perror ("recv");
return -1;
}
if (rb == 0)
{
printf ("客户机:服务器已宕机!\n");
break;
}
printf ("< %s\n", buf);
}
printf ("客户机:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("客户机:大功告成!\n");
return 0;
}
四、基于UDP协议的客户机/服务器模型
1. 基本特征
-
面向无连接。
-
不可靠,不保证数据的完整性和有序性。
-
效率高速度快。
2. 编程模型
3. 常用函数
#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void* buf, size_t len,int flags, struct sockaddr* src_addr,socklen_t* addrlen);
通过sockfd参数所标识的套接字,期望接收len个字节到buf所指向的缓冲区中。
若src_addr和addrlen参数不是空指针,则通过这两个参数输出源地址结构及其长度。
注意在这种情况下,addrlen参数的目标应被初始化为,src_addr参数的目标数据结构的大小。
成功返回实际接收到的字节数,失败返回-1。
ssize_t sendto (int sockfd, const void* buf,size_t len, int flags,const struct sockaddr* dest_addr,socklen_t addrlen);
通过sockfd参数所标识的套接字,从buf所指向的缓冲区中发送len个字节。
发送目的的地址结构及其长度,通过dest_addr和addrlen参数输入。
成功返回实际被发送的字节数,失败返回-1。
范例:udpsvr.c、udpcli.c
udpsvr.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 2)
{
fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
return -1;
}
printf ("服务器:创建网络数据报套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("服务器:准备地址并绑定...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[1]));
addr.sin_addr.s_addr = INADDR_ANY;
if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1)
{
perror ("bind");
return -1;
}
for (;;)
{
printf ("服务器:接收请求...\n");
char buf[1024];
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
ssize_t rb = recvfrom (sockfd, buf, sizeof (buf), 0,
(struct sockaddr*)&addrcli, &addrlen);
if (rb == -1)
{
perror ("recvfrom");
return -1;
}
printf ("服务器:向%s:%u客户机发送响应...\n",
inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));
if (sendto (sockfd, buf, rb, 0,
(struct sockaddr*)&addrcli, addrlen) == -1)
{
perror ("sendto");
return -1;
}
}
printf ("服务器:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
udpcli.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main (int argc, char* argv[])
{
if (argc < 3)
{
fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
return -1;
}
printf ("客户机:创建网络数据报套接字...\n");
int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1)
{
perror ("socket");
return -1;
}
printf ("客户机:准备地址...\n");
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons (atoi (argv[2]));
addr.sin_addr.s_addr = inet_addr (argv[1]);
printf ("客户机:发送请求并接收响应...\n");
for (;;)
{
printf ("%s:%u> ", inet_ntoa (addr.sin_addr),
ntohs (addr.sin_port));
char buf[1024];
gets (buf);
if (! strcmp (buf, "!")) break;
if (sendto (sockfd, buf, (strlen (buf) + 1) *
sizeof (buf[0]), 0, (struct sockaddr*)&addr,
sizeof (addr)) == -1)
{
perror ("send");
return -1;
}
struct sockaddr_in addrsvr = {};
socklen_t addrlen = sizeof (addrsvr);
if (recvfrom (sockfd, buf, sizeof (buf), 0,
(struct sockaddr*)&addrsvr, &addrlen) == -1)
{
perror ("recv");
return -1;
}
printf ("%s:%u< %s\n", inet_ntoa (addrsvr.sin_addr),
ntohs (addrsvr.sin_port), buf);
}
printf ("客户机:关闭套接字...\n");
if (close (sockfd) == -1)
{
perror ("close");
return -1;
}
printf ("客户机:大功告成!\n");
return 0;
}