网络通信实例
客户端与服务器模型又叫主从式架构,简称 C/S 结构,是一种网络架构,它把客户端 (Client)与服务器(Server)区分开来。每一个客户端应用实例都可以向一个服务器发出请求。有很多不同类型的服务器,例如文件服务器、终端服务器和邮件服务器等。虽然它们的存在的目的不一样,但基本构架是一样的。
这个方法通过不同的途径应用于很多不同类型的应用,最常见就是目前在因特网上用的网页。例如,当你在电脑上浏览 SylixOS 网站时,你的电脑和网页浏览器就被当做一个客户端,组成 SylixOS 网站的主机就被当做服务器。当你的网页浏览器向 SylixOS 站点请求一个指定的文章时,SylixOS 站点服务器从数据库中找出所有该文章需要的信息,结合成一个网页,再发送回你的浏览器。
服务端的特征:被动的角色,等待来自客户端的请求,处理请求并传回结果。
客户端的特征:主动的角色,发送请求并等待请求的响应。
UDP 实例
在使用 UDP 编写应用程序时与 TCP 编写应用程序有着本质的差异,其原因在于这两个传输层之间的差异:UDP 是不可靠的数据报协议(SylixOS 支持面向连接和非连接两种方式),不同于 TCP 提供的面向连接的可靠字节流。然而有些场合更适合使用 UDP,使用 UDP 编写的一些流行的应用程序有 DNS(域名系统)、NFS(网络文件系统)、SNMP(简单网络管理协议)。
客户端无需与服务器端建立连接,但是需要指定目的地址(服务器地址),并且客户端只管给服务器发送数据报。
服务器端不接受来自客户端的连接,而是只管等待来自某个客户端的数据。
如下图所示给出了典型的 UDP 客户端与服务器端程序的函数使用。
bind 函数把一个本地协议地址赋予一个套接字,协议地址的含义只取决于协议本身。对于网际协议,协议地址是 32 位的 IPv4 地址或 128 位 IPv6 地址与 16 位的 TCP 或 UDP 端口的组合。调用 bind 函数可以指定 IP 地址或端口,可以两者都指定,也可以都不指定。
#include <sys/socket.h>
int bind(int s, const struct sockaddr *name, socklen_t namelen);
函数 bind 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 name 是一个指向特定协议域的 sockaddr 结构体类型的指针。
- 参数 namelen 表示 name 结构的长度。
如下图所示给出了典型的 UDP 客户端与服务器端程序中 recvfrom 与 sendto 使用过程(类似于标准的 read 与 write 函数,不过需要额外三个参数)。
#include <sys/socket.h>
ssize_t recvfrom(int s, void *mem, size_t len, int flags,
struct sockaddr *from, socklen_t *fromlen);
函数 recvfrom 原型分析:
- 此函数成功时返回读取到数据的字节数,失败时返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 mem 是指向读入缓冲区的指针。
- 参数 len 表示读取数据的字节长度。
- 参数 flags 用于指定消息类型,当不关心此参数时可以将其设置为 0,如果需要关心此参数请将其值配置为以下值:。
- MSG_PEEK:数据预读但不删除数据;
- MSG_WAITALL:等待所有数据到达后才返回;
- MSG_OOB:带外数据;
- MSG_DONTWAIT:不阻塞的接收数据;
- MSG_MORE:有更多的数据需要发送。
- 参数 from 用于表示 UDP 数据报发送者的协议地址(例如 IP 地址及端口号)。
- 参数 fromlen 用于指定 from 地址大小的指针。
由于 UDP 是无连接的,因此 recvfrom 函数返回值为 0 也是有可能的。如果 from 参数是一个空指针,那么相应的长度参数 fromlen 也必须是一个空指针,表示我们并不关心数据发送者的协议地址。
#include <sys/socket.h>
ssize_t sendto(int s, const void *data, size_t size, int flags,
const struct sockaddr *to, socklen_t tolen);
函数 sendto 原型分析:
- 此函数成功时返回读取到数据的字节数,失败时返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 data 是指向写入数据缓冲区的指针。
- 参数 size 表示写入数据的字节长度。
- 参数 flags 用于指定消息类型,当不关心此参数时可以将其设置为 0,如果需要关心此参数请将其值配置为以下值:。
- MSG_PEEK:数据预读但不删除数据;
- MSG_WAITALL:等待所有数据到达后才返回;
- MSG_OOB:带外数据;
- MSG_DONTWAIT:不阻塞的接收数据;
- MSG_MORE:有更多的数据需要发送。
- 参数 to 用于表示 UDP 数据报接收者的协议地址(例如 IP 地址及端口号)。
- 参数 tolen 用于指定 to 地址长度。
sendto 函数写一个长度为 0 的数据报是可行的,这导致一个只包含 IP 头部(对于 IPv4 通常为 20 个字节,对于 IPv6 通常为 40 个字节)和一个 8 字节 UDP 头部但没有数据的 IP 数据报。
UDP 回射程序模型,如下图所示。客户端与服务器遵循该流程完成回射数据的接收与回显。
客户端程序使用 sendto 函数将“SylixOS Hello!”发送给服务器,并使用 recvfrom 读回服务器的回射,最后将收到的回射信息“SylixOS Hello!”输出。
服务器程序使用 recvfrom 函数读入来自客户端的“SylixOS Hello!”数据,并通过 sendto 把收到的数据发送给客户端程序。
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#define __UDP_ECHO_TYPE_CLIENT 1 /* 客户端模式 */
#define __UDP_ECHO_TYPE_SERVER 2 /* 服务器模式 */
/* 当前模式选择 */
#define __UDP_ECHO_TYPE (__UDP_ECHO_TYPE_SERVER)
/* 客户端 IP 地址 */
#define __UDP_ECHO_IP_CLIENT "192.168.1.16"
/* 服务器 IP 地址 */
#define __UDP_ECHO_IP_SERVER "192.168.1.17"
#define __UDP_ECHO_PORT_CLIENT 8000 /* 客户端端口号 */
#define __UDP_ECHO_PORT_SERVER 8001 /* 服务器端口号 */
#define __UDP_ECHO_BUFF_SIZE_CLIENT 257 /* 客户端接收缓冲区大小 */
#define __UDP_ECHO_BUFF_SIZE_SERVER 257 /* 服务器接收缓冲区大小 */
static int __UdpEchoServer (void)
{
int iRet = -1; /* 操作结果 */
int sockFd = -1; /* socket 描述符 */
/* 地址结构大小 */
socklen_t uiAddrLen = sizeof(struct sockaddr_in);
register ssize_t sstRecv = 0; /* 接收到的数据长度 */
/* 接收缓冲区 */
char cRecvBuff[__UDP_ECHO_BUFF_SIZE_SERVER] ={0};
struct sockaddr_in sockaddrinLocal; /* 本地地址 */
struct sockaddr_in sockaddrinRemote; /* 远端地址 */
fprintf(stdout, "UDP echo server start.\n");
/* 创建 socket */
sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockFd < 0) { /* 操作失败 */
printf("UDP echo server socket error.\n");
return (-1); /* 错误返回 */
}
/*
* 初始化本地地址结构
*/
/* 清空地址信息 */
memset(&sockaddrinLocal, 0, sizeof(sockaddrinLocal));
/* 地址结构大小 */
sockaddrinLocal.sin_len = sizeof(struct sockaddr_in);
sockaddrinLocal.sin_family = AF_INET; /* 地址族 */
/* 网络地址 */
sockaddrinLocal.sin_addr.s_addr = INADDR_ANY;
/* 绑定服务器端口 */
sockaddrinLocal.sin_port = htons(__UDP_ECHO_PORT_SERVER);
iRet = bind(sockFd,
(struct sockaddr *)&sockaddrinLocal,
sizeof(sockaddrinLocal)); /* 绑定本地地址与端口 */
if (iRet < 0) { /* 绑定操作失败 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "UDP echo server bind error.\n");
return (-1); /* 错误返回 */
}
for (;;) {
/* 清空接收缓冲区 */
memset(&cRecvBuff[0], 0, __UDP_ECHO_BUFF_SIZE_SERVER);
sstRecv = recvfrom(sockFd,
(void *)&cRecvBuff[0],
__UDP_ECHO_BUFF_SIZE_SERVER,
0,
(struct sockaddr *)&sockaddrinRemote,
&uiAddrLen); /* 从远端接收数据 */
if (sstRecv <= 0) { /* 接收数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "UDP echo server recvfrom error.\n");
return (-1);
}
continue;
}
sendto(sockFd,
(const void *)&cRecvBuff[0],
sstRecv,
0,
(const struct sockaddr *)&sockaddrinRemote,
uiAddrLen);
}
return (0);
}
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#define __UDP_ECHO_TYPE_CLIENT 1 /* 客户端模式 */
#define __UDP_ECHO_TYPE_SERVER 2 /* 服务器模式 */
/* 当前模式选择 */
#define __UDP_ECHO_TYPE (__UDP_ECHO_TYPE_CLIENT)
/* 客户端 IP 地址 */
#define __UDP_ECHO_IP_CLIENT "192.168.1.16"
/* 服务器 IP 地址 */
#define __UDP_ECHO_IP_SERVER "192.168.1.17"
#define __UDP_ECHO_PORT_CLIENT 8000 /* 客户端端口号 */
#define __UDP_ECHO_PORT_SERVER 8001 /* 服务器端口号 */
#define __UDP_ECHO_BUFF_SIZE_CLIENT 257 /* 客户端接收缓冲区大小 */
#define __UDP_ECHO_BUFF_SIZE_SERVER 257 /* 服务器接收缓冲区大小 */
static int __UdpEchoClient (void)
{
int sockFd = -1; /* socket 描述符 */
/* 地址结构大小 */
socklen_t uiAddrLen = sizeof(struct sockaddr_in);
register ssize_t sstRecv = 0; /* 接收到的数据长度 */
register ssize_t sstSend = 0; /* 接收到的数据长度 */
/* 需要发送的字符串 */
const char *pcSendData = "SylixOS Hello!\n";
/* 接收缓冲区 */
char cRecvBuff[__UDP_ECHO_BUFF_SIZE_CLIENT] ={0};
struct sockaddr_in sockaddrinRemote; /* 远端地址 */
fprintf(stdout, "UDP echo client start.\n");
/* 创建 socket */
sockFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (sockFd < 0) {
fprintf(stderr, "UDP echo client socket error.\n");
return (-1);
}
/*
* 初始化远端地址结构
*/
memset(&sockaddrinRemote, 0, sizeof(sockaddrinRemote));
/* 地址转换错误 */
if (!inet_aton(__UDP_ECHO_IP_SERVER, &sockaddrinRemote.sin_addr)) {
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "UDP echo client get addr error.\n");
return (-1); /* 错误返回 */
}
/* 地址结构大小 */
sockaddrinRemote.sin_len = sizeof(struct sockaddr_in);
sockaddrinRemote.sin_family = AF_INET; /* 地址族 */
/* 绑定服务器端口 */
sockaddrinRemote.sin_port = htons(__UDP_ECHO_PORT_SERVER);
for (;;) {
fprintf(stdout, "Send Data: %s", pcSendData);
sstRecv = strlen(pcSendData); /* 获取发送字符串长度 */
sstSend = sendto(sockFd,
(const void *)pcSendData,
sstRecv,
0,
(const struct sockaddr *)&sockaddrinRemote,
uiAddrLen); /* 发送数据到指定的服务器端 */
if (sstSend <= 0) { /* 发送数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "UDP echo client sendto error.\n");
return (-1); /* 错误返回 */
}
continue; /* 超时或非阻塞后重新运行 */
}
memset(&cRecvBuff[0], 0, __UDP_ECHO_BUFF_SIZE_CLIENT);
sstRecv = recvfrom(sockFd,
(void *)&cRecvBuff[0],
__UDP_ECHO_BUFF_SIZE_SERVER,
0,
(struct sockaddr *)&sockaddrinRemote,
&uiAddrLen); /* 从远端接收数据 */
if (sstRecv <= 0) { /* 接收数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "UDP echo client recvfrom error.\n");
return (-1); /* 错误返回 */
}
continue; /* 超时或非阻塞后重新运行 */
}
fprintf(stdout, "Recv Data: ");
cRecvBuff[sstRecv] = 0;
fprintf(stdout, "%s\n", &cRecvBuff[0]);
sleep(5); /* 休眠一段时间 */
}
return (0);
}
TCP 实例
如下图所示为典型的 TCP 客户端与服务器通信流程。服务器首先启动,稍后某个时候客户端启动,它试图连接到服务器。我们假设客户端给服务器发送一个请求,服务器处理该请求,并且给客户端发回一个响应。这个过程一直持续下去,直到客户端关闭本地连接,从而给服务器发送一个结束通知为止。服务器收到结束通知后关闭服务器端的本地连接,然后可以结束运行,也可以继续等待新的客户连接。
TCP 客户端使用 connect 函数来建立与 TCP 服务器的连接。
#include <sys/socket.h>
int connect(int s, const struct sockaddr *name, socklen_t namelen);
函数 connect 原型分析:
- 此函数成功时返回 0,失败时返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 name 是一个指向特定协议域的 sockaddr 结构体类型的指针。
- 参数 namelen 表示 name 结构的长度。
- name (套接口地址结构)必须包含服务器的 IP 地址和端口号,TCP 套接口调用 connect 函数将激发 TCP 的三次握手,而且仅在连接建立成功或出错时才返回。
listen 函数仅由 TCP 服务器调用,指示可以接受指向该套接口的连接请求。
#include <sys/socket.h>
int listen(int s, int backlog);
函数 listen 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 backlog 表示对应套接字可以接受的最大连接数。
accept 函数仅由 TCP 服务器调用,用于返回一个已完成的连接。
#include <sys/socket.h>
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);
函数 accept 原型分析:
- 此函数成功返回非负已连接套接口描述符,失败返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回); 。
- 参数 addr 用于返回已连接对端(客户端)的协议地址结构信息。
- 参数 addrlen 用于返回已连接协议地址结构大小。
我们称 accept 的第一个 s 为监听套接字(由 socket 创建,随后用作 bind 函数和 listen 函数的第一个参数),称它的返回值为已连接套接字。区分这两个套接字非常重要,一个服务器通常仅仅创建一个监听套接字,它在服务器的生命周期内一直存在,系统为每个接收到的客户端连接创建一个已连接套接字(也就是说对于它的 TCP 三次握手已经完成),当服务器完成某个客户端的服务时,相对应的已连接套接字就被关闭。
getsockname 函数用于返回与某个套接字关联的本地协议地址。
#include <sys/socket.h>
int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
函数 getsockname 原型分析:
- 此函数成功返回非 0,失败返回-1。
- 参数 s 是套接字(socket 函数返回)。
- 参数 name 用于返回本地的协议地址结构信息。
- 参数 addrlen 用于返回本地协议地址结构大小。
在一个没有调用 bind 函数的 TCP 客户端上,connect 函数成功返回后,getsockname 函数用于返回该连接的本地 IP 地址和本地端口号。
在以端口号为 0 时调用 bind 函数(告知系统选择本地端口号)后,getsockname 函数返回由系统指定的本地端口号。
在一个以通配 IP 地址调用 bind 函数之后的 TCP 服务器上,与某个客户端的连接一旦建立(accept 成功返回),getsockname 函数就可以用于返回该连接的本地 IP 地址,在这样的调用中,套接字参数必须是已连接套接字,而不是监听套接字。
getpeername 函数用于返回与某个套接字关联的远端协议地址。
#include <sys/socket.h>
int getpeername(int s, struct sockaddr *name, socklen_t *namelen);
函数 getsockname 原型分析:
- 此函数成功返回 0,失败返回-1 并设置错误号。
- 参数 s 是套接字(socket 函数返回)。
- 参数 addr 用于返回已连接对端(客户端)的协议地址结构信息。
- 参数 addrlen 用于返回已连接协议地址结构大小。
TCP 通信总结:所有客户端和服务器都从调用 socket 开始,它返回一个套接字,客户端随后调用 connect 函数,服务器则调用 bind、listen 和 accept 函数,套接字通常使用标准的 close 函数关闭,不过还可以使用 shutdown 函数关闭套接字。大多数 TCP 服务器是并发的,它们为每个待处理的客户端连接单独服务,而大多数 UDP 服务器却是迭代的。
TCP 回射程序模型如下图所示。客户端与服务器遵循该流程完成回射数据的接收与回显。
客户端程序使用 write 将“SylixOS Hello!”发送给服务器,并使用 read 读回服务器的回射,并将收到的回射信息“SylixOS Hello!”输出。
服务器程序使用 read 读入来自客户端的数据,并通过 write 把收到的数据发送给客户端程序。
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#define __TCP_ECHO_TYPE_CLIENT 1 /* 客户端模式 */
#define __TCP_ECHO_TYPE_SERVER 2 /* 服务器模式 */
/* 当前模式选择 */
#define __TCP_ECHO_TYPE (__TCP_ECHO_TYPE_SERVER)
/* 客户端 IP 地址 */
#define __TCP_ECHO_IP_CLIENT "192.168.1.16"
/* 服务器 IP 地址 */
#define __TCP_ECHO_IP_SERVER "192.168.1.17"
#define __TCP_ECHO_PORT_CLIENT 8100 /* 客户端端口号 */
#define __TCP_ECHO_PORT_SERVER 8101 /* 服务器端口号 */
#define __TCP_ECHO_BUFF_SIZE_CLIENT 257 /* 客户端接收缓冲区大小 */
#define __TCP_ECHO_BUFF_SIZE_SERVER 257 /* 服务器接收缓冲区大小 */
static int __TcpEchoServer (void)
{
int iRet = -1;
int sockFd = -1;
int sockFdNew = -1;
/* 地址结构大小 */
socklen_t uiAddrLen = sizeof(struct sockaddr_in);
register ssize_t sstRecv = 0; /* 接收到的数据长度 */
/* 接收缓冲区 */
char cRecvBuff[__TCP_ECHO_BUFF_SIZE_SERVER] ={0};
struct sockaddr_in sockaddrinLocal; /* 本地地址 */
struct sockaddr_in sockaddrinRemote; /* 远端地址 */
fprintf(stdout, "TCP echo server start.\n");
sockFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockFd < 0) {
fprintf(stderr, "TCP echo server socket error.\n");
return (-1);
}
/*
* 初始化本地地址结构
*/
memset(&sockaddrinLocal, 0, sizeof(sockaddrinLocal));
sockaddrinLocal.sin_len = sizeof(struct sockaddr_in);
/* 地址结构大小 */
sockaddrinLocal.sin_family = AF_INET; /* 地址族 */
sockaddrinLocal.sin_addr.s_addr = INADDR_ANY;
/* 绑定服务器端口 */
sockaddrinLocal.sin_port = htons(__TCP_ECHO_PORT_SERVER);
iRet = bind(sockFd,
(struct sockaddr *)&sockaddrinLocal,
sizeof(sockaddrinLocal)); /* 绑定本地地址与端口 */
if (iRet < 0) { /* 绑定操作失败 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "TCP echo server bind error.\n");
return (-1); /* 错误返回 */
}
listen(sockFd, 2);
sockFdNew = accept(sockFd, (struct sockaddr *)&sockaddrinRemote, &uiAddrLen);
if (sockFdNew < 0) {
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "TCP echo server accept error.\n");
return (-1); /* 错误返回 */
}
for (;;) {
/* 清空接收缓冲区 */
memset(&cRecvBuff[0], 0, __TCP_ECHO_BUFF_SIZE_SERVER)
/* 从远端接收数据 */
sstRecv = read(sockFdNew,
(void *)&cRecvBuff[0],
__TCP_ECHO_BUFF_SIZE_SERVER);
if (sstRecv <= 0) { /* 接收数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFdNew); /* 关闭已经连接的 socket */
fprintf(stderr, "TCP echo server recvfrom error.\n");
return (-1); /* 错误返回 */
}
continue; /* 超时或非阻塞后重新运行 */
}
/* 将回射数据发回远端 */
write(sockFdNew, (const void *)&cRecvBuff[0], sstRecv);
}
close(sockFdNew);
return (0);
}
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#define __TCP_ECHO_TYPE_CLIENT 1 /* 客户端模式 */
#define __TCP_ECHO_TYPE_SERVER 2 /* 服务器模式 */
/* 当前模式选择 */
#define __TCP_ECHO_TYPE (__TCP_ECHO_TYPE_ CLIENT)
/* 客户端 IP 地址 */
#define __TCP_ECHO_IP_CLIENT "192.168.1.16"
/* 服务器 IP 地址 */
#define __TCP_ECHO_IP_SERVER "192.168.1.17"
#define __TCP_ECHO_PORT_CLIENT 8100 /* 客户端端口号 */
#define __TCP_ECHO_PORT_SERVER 8101 /* 服务器端口号 */
#define __TCP_ECHO_BUFF_SIZE_CLIENT 257 /* 客户端接收缓冲区大小 */
#define __TCP_ECHO_BUFF_SIZE_SERVER 257 /* 服务器接收缓冲区大小 */
static int __TcpEchoClient (void)
{
int iRet = -1; /* 操作结果返回值 */
int sockFd = -1; /* socket 描述符 */
/* 地址结构大小 */
socklen_t uiAddrLen = sizeof(struct sockaddr_in);
register ssize_t sstRecv = 0; /* 接收到的数据长度 */
register ssize_t sstSend = 0; /* 接收到的数据长度 */
/* 需要发送的字符串 */
const char *pcSendData = "SylixOS Hello!\n";
/* 接收缓冲区 */
char cRecvBuff[__TCP_ECHO_BUFF_SIZE_CLIENT] ={0};
struct sockaddr_in sockaddrinRemote; /* 远端地址 */
fprintf(stdout, "TCP echo client start.\n");
sockFd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sockFd < 0) {
fprintf(stderr, "TCP echo client socket error.\n");
return (-1);
}
/*
* 初始化远端地址结构
*/
/* 清空地址信息 */
memset(&sockaddrinRemote, 0, sizeof(sockaddrinRemote));
/* 地址转换错误 */
if (!inet_aton(__TCP_ECHO_IP_SERVER, &sockaddrinRemote.sin_addr)) {
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "TCP echo client get addr error.\n");
return (-1); /* 错误返回 */
}
/* 地址结构大小 */
sockaddrinRemote.sin_len = sizeof(struct sockaddr_in);
sockaddrinRemote.sin_family = AF_INET; /* 地址族 */
/* 绑定服务器端口 */
sockaddrinRemote.sin_port = htons(__TCP_ECHO_PORT_SERVER);
iRet = connect(sockFd,
(const struct sockaddr *)&sockaddrinRemote,
uiAddrLen);
if (iRet < 0) { /* 操作失败 */
fprintf(stderr, "TCP echo client connect error.\n");
return (-1); /* 错误返回 */
}
for (;;) {
fprintf(stdout, "Send Data: %s", pcSendData);
sstRecv = strlen(pcSendData); /* 获取发送字符串长度 */
sstSend = write(sockFd,
(const void *)pcSendData,
sstRecv); /* 发送数据到指定的服务器端 */
if (sstSend <= 0) { /* 发送数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "TCP echo client write error.\n");
return (-1); /* 错误返回 */
}
continue; /* 超时或非阻塞后重新运行 */
}
/* 清空接收缓冲区 */
memset(&cRecvBuff[0], 0, __TCP_ECHO_BUFF_SIZE_CLIENT);
/* 从远端接收数据 */
sstRecv = read(sockFd,
(void *)&cRecvBuff[0],
__TCP_ECHO_BUFF_SIZE_SERVER);
if (sstRecv <= 0) { /* 接收数据失败 */
if ((errno != ETIMEDOUT ) &&
(errno != EWOULDBLOCK)) { /* 非超时与非阻塞 */
close(sockFd); /* 关闭已经创建的 socket */
fprintf(stderr, "TCP echo client read error.\n");
return (-1); /* 错误返回 */
}
continue; /* 超时或非阻塞后重新运行 */
}
fprintf(stdout, "Recv Data: ");
cRecvBuff[sstRecv] = 0;
fprintf(stdout, "%s\n", &cRecvBuff[0]);
sleep(5); /* 休眠一段时间 */
}
return (0);
}
RAW 实例
原始套接字可以提供以下 TCP 及 UDP 套接字一般不提供的功能。
- 使用原始套接字可以读、写 ICMPv4、IGMPv4 和 IGMPv6 分组。例如: ping 命令程序。
- 使用原始套接字可以读、写特殊的 IPv4 数据报,如下图中协议字段,大多数内核只处理 1(ICMP)、2(IGMP)、6(TCP)、17(UDP)数据报,但协议字段还可能为其他值,例如:OSPF 路由协议就不使用 TCP 或 UDP,而直接使用 IP,且将 IP 数据报的协议字段设为 89。因此,这些数据报包含了内核完全不知道的协议字段,就需要使用原始套接字来实现,这些同样适用于 IPv6。
RAW 套接字创建
为了创建一个原始套接字涉及以下几步:
- 当 socket 函数第二个参数是 SOCK_RAW 时,将创建一个原始套接字,第三个参数通常不为 0,下面代码是创建一个 IPv4 原始套接字:
int sockfd;
sockfd = socket(AF_INET, SOCK_RAW, protocol);
注意:
其中 protocol 参数值为 IPPROTO_xxx 常量值,如 IPPROTO_ICMP。
- 可以设置 socket 选项。
- 可以对原始套接字调用 bind 函数,但并不常见。该函数仅用来设置本地地址,对于一个原始套接字而言端口号没有任何意义。
- 在原始套接字上可以调用 connect 函数,但也不常用,connect 函数仅设置目的地址。对于输出而言,调用 connect 函数之后,由于目的地址已经指定,我们可以调用 write 函数或 send 函数,而不是 sendto 函数了。
RAW 套接字输出
通常原始套接字的输出可以通过调用 sendto 函数或 sendmsg 函数并指定目的 IP 地址来完成,如果 socket 已经调用 connect 函数进行了连接,则也可以调用 write 函数、writev 函数或 send 函数来完成。
RAW 套接字输入
对于原始套接字输入,需要考虑接收到的哪些 IP 分组将传递给原始套接字,这些需要遵守以下规则:
- 接收到 TCP 分组和 UDP 分组决不会传递给任何原始套接字,如果希望读取包含 TCP 分组或 UDP 分组的 IP 数据报,那么它们必须在链路层(见“链路层”节)读入。
- 当内核处理完 ICMP 消息后,绝大部分 ICMP 分组将传递给原始套接字。
- 当内核处理完 IGMP 消息后,所有 IGMP 分组都将传递给某个原始套接字。
- 所有带有内核不能识别的协议字段的 IP 数据报都将传递给某个原始套接字。
- 如果数据报以片段形式到达,则该分组将在所有片段到达并重组后才传给原始套接字。
当内核准备好一个待传递的数据报之后,内核将对所有的原始套接字进行检查,以寻找所有匹配的套接字,每个匹配的套接字都将收到一个该 IP 数据报的拷贝,当满足下面条件时,数据报才会发送到指定的套接字:
- 如果在创建原始套接字时,所指定的 protocol 参数不为零,则接收到的数据报的协议字段应与该值匹配,否则该数据报不会发送给该套接字。
- 如果此原始套接字之上绑定了一个本地 IP 地址,那么接收到的数据报的目的 IP 地址应与该绑定地址相匹配,否则该数据报不会发送到该原始套接字。
- 如果此原始套接字通过调用 connect 函数指定了一个对方的 IP 地址,那么接收到的数据报的源 IP 地址应与该已连接地址相匹配,否则该数据报不会发送给该原始套接字。
下面程序使用原始套接字来发送 TCP 网络数据报,数据报由我们自己来构造 IP 头和 TCP 头,分别由 ip_packet_ctor 函数和 tcp_packet_ctor 函数来完成。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <netinet/tcp.h>
#define DEST_PROT (4000)
#define PACKET_LEN (128)
#define TTLVAL (255)
void ip_packet_ctor (struct ip *iphdr, struct sockaddr_in *dest)
{
int ip_len = sizeof(struct ip) + sizeof(struct tcphdr);
iphdr->ip_v = IPVERSION;
iphdr->ip_hl = sizeof(struct ip) >> 2;
iphdr->ip_tos = 0;
iphdr->ip_len = htons(ip_len);
iphdr->ip_id = 0;
iphdr->ip_off = 0;
iphdr->ip_ttl = TTLVAL;
iphdr->ip_p = IPPROTO_TCP;
iphdr->ip_sum = 0;
iphdr->ip_dst.s_addr = dest->sin_addr.s_addr;
}
void tcp_packet_ctor (struct tcphdr *tcphdr, u16_t srcprot, struct sockaddr_in *dest)
{
tcphdr->th_sport = htons(srcprot);
tcphdr->th_dport = dest->sin_port;
tcphdr->th_seq = 3;
tcphdr->th_ack = 0;
tcphdr->th_sum = 0;
tcphdr->th_off = 5;
tcphdr->th_flags = TH_SYN;
}
u16_t tcp_chksum (u16_t *addr, int len)
{
int nleft = len;
int sum = 0;
u16_t *temp = addr;
short ans = 0;
while (nleft > 1) {
sum += *temp++;
nleft -= 2;
}
if (nleft == 1) {
*(unsigned char *)(&ans) = *(unsigned short *)temp;
sum += ans;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
ans = ~sum;
return (ans);
}
void send_packet (int sockfd, unsigned short srcport,
char *src, struct sockaddr_in *dest)
{
struct ip *iphdr;
struct tcphdr *tcphdr;
char buf[PACKET_LEN] = {0};
struct in_addr srcaddr;
int ip_len;
ip_len = sizeof(struct ip) + sizeof(struct tcphdr);
fprintf(stdout, "ip len: %d\n", ip_len);
if (src) {
inet_aton(src, &srcaddr);
}
iphdr = (struct ip *)buf;
ip_packet_ctor(iphdr, dest);
tcphdr = (struct tcphdr *)(buf + sizeof(struct ip));
tcp_packet_ctor(tcphdr, srcport, dest);
while (1) {
iphdr->ip_src.s_addr = (src == NULL) ? random() : srcaddr.s_addr;
tcphdr->th_sum = tcp_chksum((unsigned short *)tcphdr, sizeof(struct tcphdr));
sendto(sockfd, buf, ip_len, 0, (struct sockaddr *)dest, sizeof(struct sockaddr_in));
}
}
int main (int argc, char *argv[])
{
int sockfd;
struct sockaddr_in destaddr;
if (argc < 2) {
fprintf(stderr, "%s src-addr dest addr.\n", argv[0]);
return (-1);
}
sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
if (sockfd < 0) {
perror("socket");
return (-1);
}
bzero(&destaddr, sizeof(destaddr));
destaddr.sin_family = AF_INET;
destaddr.sin_len = sizeof(destaddr);
destaddr.sin_port = htons(DEST_PROT);
if (inet_aton(argv[2], &destaddr.sin_addr) == 0) {
fprintf(stderr, "destination addr don't found.\n");
return (-1);
}
send_packet(sockfd, DEST_PROT, argv[1], &destaddr);
return (0);
}