1. 基本概念
ISO/OSI 七层网络协议模型
===================================================================
-+-------------+-----------------+-
| 应用层 | Application |
-+-------------+-----------------+-
| 表示层 | Presentation |
-+-------------+-----------------+----------------
| 会话层 | Session | 创建连接
-+-------------+-----------------+-
| 传输层 | Transport |
-+-------------+-----------------+-
| 网络层 | Network |
-+-------------+-----------------+-
| 数据链路层 | Data Link | 分组、整合
-+-------------+-----------------+-
| 物理层 | Physical |
-+-------------+-----------------+-----------------
TCP/IP 协议族
========================================================================
1) TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
2) UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
3) IP (Internet Protocol, 互联网协议) 信息传递机制;TCP/UDP 低层的协议;
TCP/IP 协议与 ISO/OSI 模型的对比
=======================================================================
ISO/OSI TCP/IP
+--------------+-------------+----------------------------
| 应用层 | | |
+--------------+ | | 高
| 表示层 | 应用层 | TELNET/FTP/HTTP |
+--------------+ | | 层
| 会话层 | | |
+--------------+-------------+----------------------------
| 传输层 | 传输层 | TCP/UDP |
+--------------+-------------+---------------------+
| 网络层 | 互联网层 | IP/路由 | 低
+--------------+-------------+---------------------+
| 数据链路层 | | | 层
+--------------+ 网络接口层 | 驱动/设备 |
| 物理层 | | |
+--------------+-------------+----------------------------
消息包
=====================
+-----------------+
| TELNET/FTP/HTTP |
+-----------------+
| TCP/UDP |
+-----------------+
| IP |
+-----------------+
| ETHERNET |
+-----------------+
从上至下,消息包逐层递增,
从下至上,消息包逐层递减。
IP 地址
========================================
1) IP 地址是 Internet 中唯一的地址标识
A. 一个 IP 地址占 32 位,正在扩充至 128 位。
B. 每个 Internet 包必须带 IP 地址。
2) 点分十进制表示法
0x01020304 -> 1.2.3.4,高数位在左,低数位在右。
3) IP地址分级
A 级:0 + 7 位网络地址 + 24 位本地地址
B 级:10 + 14 位网络地址 + 16 位本地地址
C 级:110 + 21 位网络地址 + 8 位本地地址
D 级:1110 + 28 位多播 (Muticast) 地址
4) 子网掩码
IP 地址 & 子网掩码 = 网络地址
IP 地址: 192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182
本地地址:48
127.0.0.1: 回绕地址,表示本机,不依赖网络。
2. 套接字 (Socket)
接口:
PuTTY -> telnet \
LeapFTP -> ftp -> socket -> TCP/UDP -> IP -> 网卡驱动 -> 网卡硬件
IE -> http / ^
|
应用程序 -----------------+
异构:
Java @ UNIX -> socket <----> socket <- C/C++ @ Windows
模式:
1) 点对点 (Peer-to-Peer, P2P):一对一的通信。
2) 客户机/服务器 (Client/Server, C/S):一对多的通信。
绑定:
先要有一个套接字描述符,还要有物理通信载体,然后将二者绑定在一起。
函数:
1) 创建套接字
#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。
套接字描述符类似于文件描述符,UNIX 把网络当文件看待,
发送数据即写文件,接收数据即读文件,一切皆文件。
2) 准备通信地址
A. 基本地址类型
struct sockaddr {
sa_family_t sa_family; // 地址族(取值和上面 domain 一样)
char sa_data[14]; // 地址值
};
B. 本地地址类型
#include <sys/un.h> // un 指 unix
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;
IP 地址用于定位主机,端口号用于定位主机上的进程。
3) 将套接字和通信地址绑定在一起
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
sockfd:socked 描述符;
addr:可以是 sockaddr_un,也可以是 sockaddr_in;
addrlen:地址长度;
成功返回 0,失败返回 -1。
4) 建立连接
#include <sys/socket.h>
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回 0,失败返回 -1。
5) 用读写文件的方式通信:read/write
6) 关闭套接字:close
7) 字节序转换
#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);
主机字节序因处理器架构而异,有的采用小端字节序,有的采用大端字节序。
网络字节序则固定采用大端字节序。
8) 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);
编程:
1) 本地通信
服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->发送数据->关闭套接字
范例:locsvr.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
// 本地通信需要路径;
#define SOCK_FILE "/tmp/sock"
int main (void) {
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];
// 一切皆文件思想:read 可以用于接受数据;
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 <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "/tmp/sock"
int main (void) {
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;
// 一切皆文件思想:write 可以用于发送;
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;
}
测试运行的时候需要开启两个窗口,分别运行客户端和服务器端;
2) 网络通信
服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发送数据->关闭套接字
范例:netsvr.c
#include <stdio.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); // UDP
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; // IP 地址,计算机自动分配;
// addr.sin_addr.s_addr = inet_addr ("127.0.0.1");
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 <string.h>
#include <sys/socket.h>
#include <netinet/in.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]); // 手动获得 IP 地址;
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;
}
运行测试:
首先通过下面命令查看服务器 ip 地址;
#sbin/ifconfig
然后在客户端输入 ip 地址和端口号,即可建立连接;
3. 基于 TCP 协议的客户机/服务器模型
基本特征:
1) 面向连接。
2) 可靠,保证数据的完整性和有序性。
ABCDEF
A -> -
B -> |
C -> +- 时间窗口
D -> |
E -> <- A OK -
F -> <- B OK
<- C OK
<- D OK
<- E OK
<- F OK
每个发送都有应答,若在时间窗口内没有收到 A 的应答,则从 A 开始重发。
编程模型:
-------+-------------------------+----------------------------+------
步骤| 服务器 | 客户机 | 步骤
-------+--------------+----------+-------------+--------------+------
1 | 创建套接字 | socket | socket | 创建套接字 | 1
2 | 准备地址 | . . . | . . . | 准备地址 | 2
3 | 绑定套接字 | bind | | ---- |
4 | 监听套接字 | listen | | ---- |
5 | 接受连接 | accept | connect | 建立链接 | 3
6 | 接收请求 | recv | send | 发送请求 | 4
7 | 发送响应 | send | recv | 接收响应 | 5
8 | 关闭套接字 | close | close | 关闭套接字 | 6
-------+-------------+-----------+-------------+--------------+------
常用函数:
#include <sys/socket.h>
int listen (int sockfd, int backlog);
1)将 sockfd 参数所标识的套接字标记为被动模式,使之可用于接受连接请求。
2)backlog 参数表示未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。
若服务器的未决连接数已达上限,则客户机端的 connect() 函数返回 -1;且 errno 为 ECONNREFUSED;
3)成功返回 0,失败返回 -1。
int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
1)从 sockfd 参数所标识套接字的未决连接请求队列中,
提取第一个连接请求,同时创建一个新的套接字,
用于在该连接中通信,返回该套接字的描述符。
2)addr 和 addrlen 参数用于输出连接请求发起者的地址信息。
3)成功返回通信套接字描述符,失败返回 -1。
ssize_t recv (int sockfd, void* buf, size_t len, int flags);
1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
2)成功返回实际接收到的字节数,失败返回 -1。
ssize_t send (int sockfd, const void* buf, size_t len, int flags);
1)通过 sockfd 参数所标识的套接字,从 buf 所指向的缓冲区中发送 len 个字节。
2)成功返回实际被发送的字节数,失败返回 -1。
范例:tcpsvr.c
#include <stdio.h>
#include <stdlib.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); // waitpid 默认非阻塞模式;
// waitpid 失败返回 -1;
if (pid == -1) {
if (errno != ECHILD) {
perror ("waitpid");
exit (-1);
}
// 子进程全部回收完毕 waitpid 返回 ECHILD;
printf ("服务器:全部子进程都已退出。\n");
break;
}
// waitpid 正常情况下返回被回收子进程的 pid
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); // TCP
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");
// 定义地址结构,给 accept 参数调用;
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
// accept 返回连接套接字;后两个参数是地址,而非数值;
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 ());
// 通过 recev 创建连接套接字;
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;
}
}
// 第七步:关闭服务器监听套接字;
printf ("服务器:关闭监听套接字...\n");
if (close (sockfd) == -1) {
perror ("close");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
注意:我们一开始创建的 socket 在 listen 阶段,我们给他加了“耳朵”,
那么这个 socket 只用于监听,不用于后期的数据传输;
在 accept 阶段,会返回一个 socket,这个 socket 是用于传输用的;
范例:tcpcli.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.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) { // 0 非阻塞方式
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;
}
4. 基于 UDP 协议的客户机/服务器模型
基本特征
1) 无连接。
2) 不可靠,不保证数据的完整性和有序性。
A
/ \
/ \
ABC ->+-(B)-+-> CA
\ /
\ /
C
效率高速度快。
编程模型
-------+----------------------------+----------------------------+------
步骤| 服务器 | 客户机 | 步骤
-------+--------------+-------------+-------------+--------------+------
1 | 创建套接字 | socket | socket | 创建套接字 | 1
2 | 准备地址 | . . . | . . . | 准备地址 | 2
3 | 绑定套接字 | bind | | ---- |
4 | 接收请求 | recvfrom | sendto | 发送请求 | 3
5 | 发送响应 | sendto | recvfrom | 接收响应 | 4
6 | 关闭套接字 | close | close | 关闭套接字 | 5
-------+-------------+--------------+-------------+--------------+------
常用函数
#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void* buf, size_t len,
int flags, struct sockaddr* src_addr, socklen_t* addrlen);
1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
2)若 src_addr 和 addrlen 参数不是空指针,则通过这两个参数输出源地址结构及其长度。
注意在这种情况下,addrlen 参数的目标应被初始化为 src_addr 参数的目标数据结构的大小。
3)flags 为 0 表示阻塞模式;
4)成功返回实际接收到的字节数,失败返回 -1。
ssize_t sendto (int sockfd, const void* buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
1)通过 sockfd 参数所标识的套接字,
从 buf 所指向的缓冲区中发送 len 个字节。
2)发送目的的地址结构及其长度,
通过 dest_addr 和 addrlen 参数输入。
3)成功返回实际被发送的字节数,失败返回 -1。
范例:udpsvr.c
#include <stdio.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); // UDP
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, // 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, // 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 <string.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;
}
练习:基于TCP协议的网络银行。
代码:参见 /项目/bank2
ISO/OSI 七层网络协议模型
===================================================================
-+-------------+-----------------+-
| 应用层 | Application |
-+-------------+-----------------+-
| 表示层 | Presentation |
-+-------------+-----------------+----------------
| 会话层 | Session | 创建连接
-+-------------+-----------------+-
| 传输层 | Transport |
-+-------------+-----------------+-
| 网络层 | Network |
-+-------------+-----------------+-
| 数据链路层 | Data Link | 分组、整合
-+-------------+-----------------+-
| 物理层 | Physical |
-+-------------+-----------------+-----------------
TCP/IP 协议族
========================================================================
1) TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
2) UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
3) IP (Internet Protocol, 互联网协议) 信息传递机制;TCP/UDP 低层的协议;
TCP/IP 协议与 ISO/OSI 模型的对比
=======================================================================
ISO/OSI TCP/IP
+--------------+-------------+----------------------------
| 应用层 | | |
+--------------+ | | 高
| 表示层 | 应用层 | TELNET/FTP/HTTP |
+--------------+ | | 层
| 会话层 | | |
+--------------+-------------+----------------------------
| 传输层 | 传输层 | TCP/UDP |
+--------------+-------------+---------------------+
| 网络层 | 互联网层 | IP/路由 | 低
+--------------+-------------+---------------------+
| 数据链路层 | | | 层
+--------------+ 网络接口层 | 驱动/设备 |
| 物理层 | | |
+--------------+-------------+----------------------------
消息包
=====================
+-----------------+
| TELNET/FTP/HTTP |
+-----------------+
| TCP/UDP |
+-----------------+
| IP |
+-----------------+
| ETHERNET |
+-----------------+
从上至下,消息包逐层递增,
从下至上,消息包逐层递减。
IP 地址
========================================
1) IP 地址是 Internet 中唯一的地址标识
A. 一个 IP 地址占 32 位,正在扩充至 128 位。
B. 每个 Internet 包必须带 IP 地址。
2) 点分十进制表示法
0x01020304 -> 1.2.3.4,高数位在左,低数位在右。
3) IP地址分级
A 级:0 + 7 位网络地址 + 24 位本地地址
B 级:10 + 14 位网络地址 + 16 位本地地址
C 级:110 + 21 位网络地址 + 8 位本地地址
D 级:1110 + 28 位多播 (Muticast) 地址
4) 子网掩码
IP 地址 & 子网掩码 = 网络地址
IP 地址: 192.168.182.48
子网掩码:255.255.255.0
网络地址:192.168.182
本地地址:48
127.0.0.1: 回绕地址,表示本机,不依赖网络。
2. 套接字 (Socket)
接口:
PuTTY -> telnet \
LeapFTP -> ftp -> socket -> TCP/UDP -> IP -> 网卡驱动 -> 网卡硬件
IE -> http / ^
|
应用程序 -----------------+
异构:
Java @ UNIX -> socket <----> socket <- C/C++ @ Windows
模式:
1) 点对点 (Peer-to-Peer, P2P):一对一的通信。
2) 客户机/服务器 (Client/Server, C/S):一对多的通信。
绑定:
先要有一个套接字描述符,还要有物理通信载体,然后将二者绑定在一起。
函数:
1) 创建套接字
#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。
套接字描述符类似于文件描述符,UNIX 把网络当文件看待,
发送数据即写文件,接收数据即读文件,一切皆文件。
2) 准备通信地址
A. 基本地址类型
struct sockaddr {
sa_family_t sa_family; // 地址族(取值和上面 domain 一样)
char sa_data[14]; // 地址值
};
B. 本地地址类型
#include <sys/un.h> // un 指 unix
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;
IP 地址用于定位主机,端口号用于定位主机上的进程。
3) 将套接字和通信地址绑定在一起
#include <sys/socket.h>
int bind (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
sockfd:socked 描述符;
addr:可以是 sockaddr_un,也可以是 sockaddr_in;
addrlen:地址长度;
成功返回 0,失败返回 -1。
4) 建立连接
#include <sys/socket.h>
int connect (int sockfd, const struct sockaddr* addr, socklen_t addrlen);
成功返回 0,失败返回 -1。
5) 用读写文件的方式通信:read/write
6) 关闭套接字:close
7) 字节序转换
#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);
主机字节序因处理器架构而异,有的采用小端字节序,有的采用大端字节序。
网络字节序则固定采用大端字节序。
8) 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);
编程:
1) 本地通信
服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->发送数据->关闭套接字
范例:locsvr.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
// 本地通信需要路径;
#define SOCK_FILE "/tmp/sock"
int main (void) {
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];
// 一切皆文件思想:read 可以用于接受数据;
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 <sys/socket.h>
#include <sys/un.h>
#define SOCK_FILE "/tmp/sock"
int main (void) {
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;
// 一切皆文件思想:write 可以用于发送;
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;
}
测试运行的时候需要开启两个窗口,分别运行客户端和服务器端;
2) 网络通信
服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接收数据->关闭套接字
客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发送数据->关闭套接字
范例:netsvr.c
#include <stdio.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); // UDP
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; // IP 地址,计算机自动分配;
// addr.sin_addr.s_addr = inet_addr ("127.0.0.1");
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 <string.h>
#include <sys/socket.h>
#include <netinet/in.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]); // 手动获得 IP 地址;
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;
}
运行测试:
首先通过下面命令查看服务器 ip 地址;
#sbin/ifconfig
然后在客户端输入 ip 地址和端口号,即可建立连接;
3. 基于 TCP 协议的客户机/服务器模型
基本特征:
1) 面向连接。
2) 可靠,保证数据的完整性和有序性。
ABCDEF
A -> -
B -> |
C -> +- 时间窗口
D -> |
E -> <- A OK -
F -> <- B OK
<- C OK
<- D OK
<- E OK
<- F OK
每个发送都有应答,若在时间窗口内没有收到 A 的应答,则从 A 开始重发。
编程模型:
-------+-------------------------+----------------------------+------
步骤| 服务器 | 客户机 | 步骤
-------+--------------+----------+-------------+--------------+------
1 | 创建套接字 | socket | socket | 创建套接字 | 1
2 | 准备地址 | . . . | . . . | 准备地址 | 2
3 | 绑定套接字 | bind | | ---- |
4 | 监听套接字 | listen | | ---- |
5 | 接受连接 | accept | connect | 建立链接 | 3
6 | 接收请求 | recv | send | 发送请求 | 4
7 | 发送响应 | send | recv | 接收响应 | 5
8 | 关闭套接字 | close | close | 关闭套接字 | 6
-------+-------------+-----------+-------------+--------------+------
常用函数:
#include <sys/socket.h>
int listen (int sockfd, int backlog);
1)将 sockfd 参数所标识的套接字标记为被动模式,使之可用于接受连接请求。
2)backlog 参数表示未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。
若服务器的未决连接数已达上限,则客户机端的 connect() 函数返回 -1;且 errno 为 ECONNREFUSED;
3)成功返回 0,失败返回 -1。
int accept (int sockfd, struct sockaddr* addr, socklen_t* addrlen);
1)从 sockfd 参数所标识套接字的未决连接请求队列中,
提取第一个连接请求,同时创建一个新的套接字,
用于在该连接中通信,返回该套接字的描述符。
2)addr 和 addrlen 参数用于输出连接请求发起者的地址信息。
3)成功返回通信套接字描述符,失败返回 -1。
ssize_t recv (int sockfd, void* buf, size_t len, int flags);
1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
2)成功返回实际接收到的字节数,失败返回 -1。
ssize_t send (int sockfd, const void* buf, size_t len, int flags);
1)通过 sockfd 参数所标识的套接字,从 buf 所指向的缓冲区中发送 len 个字节。
2)成功返回实际被发送的字节数,失败返回 -1。
范例:tcpsvr.c
#include <stdio.h>
#include <stdlib.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); // waitpid 默认非阻塞模式;
// waitpid 失败返回 -1;
if (pid == -1) {
if (errno != ECHILD) {
perror ("waitpid");
exit (-1);
}
// 子进程全部回收完毕 waitpid 返回 ECHILD;
printf ("服务器:全部子进程都已退出。\n");
break;
}
// waitpid 正常情况下返回被回收子进程的 pid
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); // TCP
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");
// 定义地址结构,给 accept 参数调用;
struct sockaddr_in addrcli = {};
socklen_t addrlen = sizeof (addrcli);
// accept 返回连接套接字;后两个参数是地址,而非数值;
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 ());
// 通过 recev 创建连接套接字;
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;
}
}
// 第七步:关闭服务器监听套接字;
printf ("服务器:关闭监听套接字...\n");
if (close (sockfd) == -1) {
perror ("close");
return -1;
}
printf ("服务器:大功告成!\n");
return 0;
}
注意:我们一开始创建的 socket 在 listen 阶段,我们给他加了“耳朵”,
那么这个 socket 只用于监听,不用于后期的数据传输;
在 accept 阶段,会返回一个 socket,这个 socket 是用于传输用的;
范例:tcpcli.c
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.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) { // 0 非阻塞方式
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;
}
4. 基于 UDP 协议的客户机/服务器模型
基本特征
1) 无连接。
2) 不可靠,不保证数据的完整性和有序性。
A
/ \
/ \
ABC ->+-(B)-+-> CA
\ /
\ /
C
效率高速度快。
编程模型
-------+----------------------------+----------------------------+------
步骤| 服务器 | 客户机 | 步骤
-------+--------------+-------------+-------------+--------------+------
1 | 创建套接字 | socket | socket | 创建套接字 | 1
2 | 准备地址 | . . . | . . . | 准备地址 | 2
3 | 绑定套接字 | bind | | ---- |
4 | 接收请求 | recvfrom | sendto | 发送请求 | 3
5 | 发送响应 | sendto | recvfrom | 接收响应 | 4
6 | 关闭套接字 | close | close | 关闭套接字 | 5
-------+-------------+--------------+-------------+--------------+------
常用函数
#include <sys/socket.h>
ssize_t recvfrom (int sockfd, void* buf, size_t len,
int flags, struct sockaddr* src_addr, socklen_t* addrlen);
1)通过 sockfd 参数所标识的套接字,期望接收 len 个字节到 buf 所指向的缓冲区中。
2)若 src_addr 和 addrlen 参数不是空指针,则通过这两个参数输出源地址结构及其长度。
注意在这种情况下,addrlen 参数的目标应被初始化为 src_addr 参数的目标数据结构的大小。
3)flags 为 0 表示阻塞模式;
4)成功返回实际接收到的字节数,失败返回 -1。
ssize_t sendto (int sockfd, const void* buf, size_t len, int flags,
const struct sockaddr* dest_addr, socklen_t addrlen);
1)通过 sockfd 参数所标识的套接字,
从 buf 所指向的缓冲区中发送 len 个字节。
2)发送目的的地址结构及其长度,
通过 dest_addr 和 addrlen 参数输入。
3)成功返回实际被发送的字节数,失败返回 -1。
范例:udpsvr.c
#include <stdio.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); // UDP
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, // 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, // 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 <string.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;
}
练习:基于TCP协议的网络银行。
代码:参见 /项目/bank2