socket 编程 (Linux IPv4)
一下讨论 TCP/IP v4 下的 socket 编程, 基于 Linux 实现.
socket 函数:
#include <sys/socket.h>
/* 主要任务是创建一个 socket 套接字, 用文件描述符(一个 int 型变量)表示.
*
* 返回:
* 成功: 此函数返回一个创建的套接字描述符(非负整数值).
* 失败: 返回 -1 并置 errno.
*/
int socket(
int family, // AF_INET | AF_INET6 | AF_ROUTE 代表 IPv4, IPv6, 路由套接口
int type, // SOCK_STREAM: 字节流(用于 TCP)
// SOCK_DGRAM: 数据报(用于 UDP)
// SOCK_RAW: 原始套接字
int protocol // 当 type 为 SOCK_RAW 时, 用于指定协议. 其它情况置零
) ;
/* 将以创建的套接字和一个特定的地址结构绑定.
*
* 返回:
* 成功: 0
* 失败: -1, 并置 errno
*/
int bind(
int fd_server, // 以创建的 socket 描述符.
const struct sockaddr *addr_server,
// 地址信息, 标识了本地端口等信息
// 一般情况下, 这里需要使用强制类型转换,
// 将 IPv4 的 struct sockaddr_in * 转换成
// 通用结构 struct sockaddr *.
socklen_t addrlen // 地址信息的长度.
) ;
/* 将套接字转换为被动套接字, 使它处于监听模式.
*
* 返回:
* 成功: 0
* 失败: -1, 并置 errno
*/
int listen(
int fd_server, // 以创建的套接字描述符.
int backlog // 最大等待队列的长度.
// DOS (拒绝服务)攻击就是利用
// 三次握手, 但不提供最后一次确认而占满
// 服务器 TCP 程序的这个队列, 从而导致服务器
// 在一定时限内不能为他人提供服务.
) ;
/* 接受来自客户端的链接请求, 此函数会阻塞, 直至有新的 TCP 链接请求到达.
*
* 返回:
* 成功: 与客户端相连接的 socket 描述符.
* 失败: -1 并置 errno.
* 此函数直至三次握手从客户端接收到最后一次握手的确认信息后才返回, 拒绝服务就是大量造成
* 无法使此函数在服务器端得到最后一次确认而导致服务器的 accept 函数一直等待, 从而服务器端
* 接收等待队列占满, 结果就是拒绝新的连接请求, 这就是所谓的拒绝服务(DOS).
*/
int accept(
int fd_server, // 以创建的 socket 描述符.
struct sockaddr *addr_client,
// 与此服务器相连接的客户端的地址信息.
socklen_t *addrLen // 地址信息的长度.
) ;
/* 激发 TCP 的三次握手. 此函数只有在链接建立成功或失败时才返回.
* 若函数调用失败, 则参数中的套接字描述符不能再次使用, 必须关闭.
* 若想继续向服务器建立连接, 需重新调用 socket() 获取套接字.
*
* 返回:
* 成功: 0.
* 失败: -1.
*/
int connect(int fd_client, // 以创建的 socket 描述符.
const struct sockaddr *addr_server,
// 远程服务器的地址信息.
socklen_t addrLen // 地址信息的长度.
) ;
#include <unistd.h>
/* 将套接字描述符引用计数减一. 当此引用计数减为 0 时, 内核自动释放其资源.
*
* 返回:
* 成功: 0.
* 失败: -1.
*/
int close(
int fd_sock // 以创建的套接字描述符.
) ;
辅助函数:
#include <arpa/inet.h> // 解析地址结构
/*
* 将字符串 IP 地址转换成网络序的
* Param:
* str: 字符串形式的 IP 地址.
* Rtn:
* 网络序地址.
* 此函数不能用于 255.255.255.255, 也就是全 1 地址.
* 所以更多的地方使用 inet_aton 代替.
*/
in_addr_t inet_addr(const char *str) ;
/*
* 将字符串 IP 地址转换成网络序地址结构
* Param:
* str: 将要被转换的字符串地址结构.
* num: 转换后的网络序地址结构.
* Rtn:
* 1: 成功.
* 0: 失败.
*/
int inet_aton(const char *str, struct in_addr *num) ;
/*
* 将网络序的 IP 地址转换成可读的字符串.
* Param:
* addr: 网络序地址结构
* Rtn:
* 可读字符串. 如 "127.0.0.1"
*/
char *inet_ntoa(struct in_addr addr) ;
数据结构:
#include <netinet/in.h>
typedef uint32_t in_addr_t ;
typedef uint16_t in_port_t ;
typedef unsigned short sa_family_t ;
struct in_addr
{
in_addr_t s_addr ; // 此字段存放的是网络序的二进制 IP 地址.
} ;
struct sockaddr_in // IPv4 地址结构
{
uint8_t sin_len ; // 存储套接字地址结构的长度, 通常不需要设置.
sa_family_t sin_family ; // 地址族, IPv4 中是 AF_INET.
in_port_t sin_port ; // 端口号, 需要网络顺序.
struct in_addr sin_addr ;// 此字段是一个结构, 这个结构的成员才是二进制 IP 地址. 需要网络顺序.
char sin_zero[8] ; // 填充字段.
} ;
struct sockaddr // 通用地址结构, 用作 socket 系列函数的参数传递.
{
uint8_t sa_len ; // 存储套接字地址结构的长度, 通常不需要设置.
sa_family_t sa_family ; // 地址族, IPv4 中是 AF_INET, IPv6 是 AF_INET6.
char sa_data[14] ; // 填充字段.
}
实例:
UDP:
个人认为, UDP 是比较容易入手. UDP 分为四步:
- socket() ; // 创建 socket 套接字
- bind() ; // 绑定 socket 描述符到特定的端口
- recvfrom() ; // 从客户端接收数据 阻塞
- close() ; // 关闭套接字
const char *strIp = "192.168.1.1" ;
unsigned short int uiPort = 4321 ;
int fd_udp = 0 ;
struct sockaddr_in addr_me ;
memset(&addr_me, '\0', sizeof(addr_me)) ;
addr_me.sin_family
// 创建套接字描述符
socket(AF_INET, SOCK_DGRAM, 0) ;
if (-1 == fd_udp)
{
perror("'socket' fail") ;
return -1 ;
}
addr_me.sin_family = AF_INET ;
addr_me.sin_addr.s_addr = inet_addr(strIp) ;
addr_me.sin_port = htons(uiPort) ;
// 将创建好的套接字描述符绑定到特定的端口.
if (-1 == bind(fd_udp, (struct sockaddr *)&addr_me, sizeof(addr_me)))
{
perror("'bind' fail") ;
return -1 ;
}//if
// UDP 发送
char data[10] = {'\0'} ;
sendto(fd_udp, data, sizeof(data),
0, // 这个是传输控制标志, 通常为 0, 表示常规操作.
(struct sockaddr *)&addr_other, // 因为 'sendto' 等 socket 相关函数为了
// 适应不同网络协议之间的差距, 特定定义了一个 "通用结构体"
// 命名为 struct sockaddr, 我们使用此函数是必须将特定的结构体
// 转换为通用结构体进行传参.
sizeof(addr_me) // 这里与 'recvfrom' 不同的是, 地址长度不需要被返回, 所以不用传指针.
) ;
// UDP 接收
char buf[10] = {'\0'} ;
struct sockaddr_in addr_other ;
memset(&addr_other, '\0', sizeof(addr_other)) ;
socklen_t size_addr_other = sizeof(addr_other) ;
recvfrom(fd_udp, buf, sizeof(buf),
0, // 同 'sendto'.
(struct sockaddr *)&addr_other,// 同 'sendto'.
&size_addr_other // 接收时需要通过此参数返回地址长度, 所以传指针.
) ;
TCP:
一般分为客户端和服务器, 因为两者代码有所区别, 这一点不同于 UDP.
- socket // 创建套接字
- bind // 绑定套接字到特定的端口
- listen // 将套接字转变为监听套模式(具体含义我也不清楚, 还请高人指点)
- accept // 接受客户端请求, 建立连接 阻塞, 直至接收到客户端三次握手的第三次确认信息后返回.
- close // 关闭套接字
// TCP Server
#define BACKLOG (10) // 服务器端最大连接数
#define PORT (1234) // 端口
int fd_server = 0 ;
struct sockaddr_in addr_server ;
memset(&addr_server, '\0', sizeof(addr_server)) ;
addr_server.sin_family = AF_INET ;
addr_server.sin_addr.s_addr = htonl(INADDR_ANY) ; // 接收来自任何地址的数据
addr_server.sin_port = htons(PORT) ; // 转换成网络序的端口
// 创建套接字
if (-1 == (fd_server = socket(AF_INET, SOCK_STREAM, 0))
{
perror("'socket' fail") ;
// Error handler
}
// 绑定套接字
if (-1 == bind(fd_server, (struct sockaddr *)&addr_server, sizeof(addr_server)))
{
perror("'bind' fail") ;
// Error handler
}
// 监听套接字
if (-1 == listen(fd_server, BACKLOG))
{
perror("'listen' fail") ;
// Error handler
}
while (true)
{
struct sockaddr_in addr_client ;
memset(&addr_client, '\0', sizeof(addr_client)) ;
socklen_t size_client = sizeof(addr_client) ;
int fd_client = 0 ;
// 接受客户端握手请求
if (-1 == accept(fd_server, (struct sockaddr *)&addr_client, &size_client))
{
perror("'accept' fail") ;
// Error handler
}
// 发送数据
char data[10] = {'\0'} ;
send(fd_server, data, sizeof(data), 0);
}//while
// 关闭套接字
close(fd_server) ;
close(fd_client) ;
客户端思路
- socket // 创建套接字
- connect // 发起三次握手 阻塞, 直至客户端接到三次握手中的第二次或者函数失败为止.
- close // 关闭套接字
// TCP Client
#define IP_SERVER "127.0.0.1"
#define PORT_SERVER 1234
int fd_client = 0 ;
struct sockaddr_in addr_client ;
memset(&addr_client, '\0', sizeof(addr_client)) ;
addr_client.sin_family = AF_INET ;
addr_cilent.sin_addr.s_addr = inet_addr(IP_SERVER) ;
addr_client.sin_port = htons(PORT_SERVER) ;
// 创建套接字
if (-1 == (fd_client = socket(AF_INET, SOCK_STREAM, 0)))
{
perror("'socket' fail ") ;
// Error handler
}
// 向服务器发起握手
if (-1 == connect(fd_client,
(struct sockaddr *)&addr_client, sizeof(addr_client)))
{
perror("'connect' fail") ;
// Error handler
}
// 接收 TCP 数据
char buf[10] = {'\0'} ;
int recvLen = recv(fd_client, buf, sizeof(buf)) ;
// 关闭套接字
close(fd_client) ;