基于Linux的套接字相关函数

在 linux 下,socket 也被认为是文件的一种。

文件的读写:

1
2
3
4
5
#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>

int open(const char* path, int flag);
1
2
3
4
5
#include <unistd.h>

ssize_t write(int fd, const void* buf, size_t nbytes);

int close(int fd);
1
2
#include <unistd.h>
ssize_t read(int fd, void* buf, size_t nbytes);

open 函数的返回值即文件描述符,也即 close 函数的参数。

ps: size_t 是通过 typedef 声明的 unsigned int 类型;ssize_t 是通过 typedef 声明的 signed int 类型。

  1. 服务端套接字创建过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/socket.h>
#include <unistd.h>

// 创建 socket
int socket(int domaink, int type, int protocol);
// 绑定ip,端口号
int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);
// 创建监听
int listen(int sockfd, int backlog);
// 接受连接
int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
// 发送数据
ssize_t write(int fd, const void* buf, size_t nbytes);
// 关闭套接字
int close(int fd);
  1. 客户端套接字创建过程
1
2
3
4
5
6
7
8
9
10
11
#include <sys/socket.h>
#include <unistd.h>

// 创建 socket
int socket(int domaink, int type, int protocol);
// 请求连接
int connect(int sockfd, struct sockaddr* serv_addr, socklen_t addrlen);
// 接收数据
ssize_t read(int fd, void* buf, size_t nbytes);
// 关闭套接字
int close(int fd);

基于Windows的套接字相关函数

1
2
3
4
#include <WinSock2.h>

// 初始化,可借助 MAKEWORD(1, 2);函数构建WORD型版本信息
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

Windows 严格区分文件 I/O 函数和套接字 I/O 函数。

1
2
3
4
5
#include <WinSock2.h>

int send(SOCKET s, const char* buf, int len, int flags);

int recv(SOCKET s, const char* buf, int len, int flags);
  1. 服务端套接字创建过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <WinSock2.h>
// 初始化
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 创建套接字
SOCKET socket(int af, int type, int protocol);
// 绑定ip,端口号
int bind(SOCKET s, const struct sockaddr* name, int namelen);
// 创建监听
int listen(SCOKET s, int backlog);
// 接受连接
SOCKET accept(SOCKET s, struct sockaddr* addr, int* addrlen);
// 发送数据
int send(SOCKET s, const char* buf, int len, int flags);
// 关闭套接字
int closesocket(SOCKET s);
// 注销初始化
int WSACleanup(void);
  1. 客户端套接字创建过程
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <WinSock2.h>
// 初始化
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
// 创建套接字
SOCKET socket(int af, int type, int protocol);
// 请求连接
int connect(SOCKET s, const char* buf, int len, int flags);
// 接收数据
int recv(SOCKET s, const char* buf, int len, int flags);
// 关闭套接字
int closesocket(SOCKET s);
// 注销初始化
int WSACleanup(void);

套接字类型及协议

int socket(int domaink, int type, int protocol);

domain

1
2
3
4
5
PF_INET     IPv4 互联网协议族
PF_INET6 IPv6 互联网协议族
PF_LOCAL 本地通信的 UNIX 协议族
PF_PACKET 底层套接字的协议族
PF_IPX IPX Novell 协议族

type

面向连接的套接字 SOCK_STREAM,特点:可靠传输,有序传输,不存在数据边界

面向消息的套接字 SOCK_DGRAM,特点:快速船务,无序传输,有数据边界,限制传输数据大小

protocol

满足 PF_INET + SOCK_STREAM 的只有 IPPROTO_TCP
满足 PF_INET + SOCK_DGRAM 的只有 IPPROTO_UDP

地址族与数据序列

int bind(int sockfd, struct sockaddr* myaddr, socklen_t addrlen);

struct sockaddr_in

1
2
3
4
5
6
7
8
9
10
11
12
struct sockaddr_in
{
sa_family_t sin_family; // 地址族
uint16_t sin_port; // 16位 TCP/UDP 端口号
struct in_addr; // 32位 IP 地址
char sin_zero[8]; // 不使用
}

struct in_addr
{
In_addr_t s_addr; //32位 IP 地址
}

sockaddr_in 成员分析

sin_family

sin_port
保存16位端口号,重点在于,它以网络字节序保存.

sin_addr
该成员保存32位IP地址信息,且也以网络字节序保存。

sin_zero
只为使 sockaddr_in 的大小与 sockaddr 结构体保持一致而插入的成员,必须填充为0。

使用举例:

1
2
3
4
5
6
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));

int bind(serv_sock, (struct sockaddr*) serv_addr, sizeof(serv_addr));

网络字节序与地址变换

在通过网络传输数据时约定统一方式,这种约定称为网络字节序,统一为大端序。

字节序转换函数:

1
2
3
4
unsigned short htons(unsigned short);
unsigned short ntols(unsigned short);
unsigned long htonl(unsigned long);
unsigned long ntohl(unsigned long);

h 代表 host, n 代表 network, s 代表 short, l 代表 long

那么 htons 即“把short型数据从网络字节序转化为主机字节序”

网络地址的初始化与分配

将点分十进制格式的字符串转为32位整型数据的函数

1
2
3
#include <arpa/inet.h>

in_addr_t inet_addr(const char* string);

与之类似的函数

1
2
3
#include <arpa/inet.h>

int inet_aton(const char* string, struct in_addr* addr);

成功时返回1,失败返回0;string 含有需要转换IP地址信息的字符串地址,将保存转换结果的 in_addr 结构体变量的地址值。

请查看该函数的源码,了解一下是怎么转换的。

介绍一个相反的函数,将32位整型数据转化为点分十进制的IP字符串形式

1
2
3
#include <arpa/inet.h>

char * inet_ntoa(struct in_addr adr);

失败时返回-1

网络地址初始化:

结合前面所述,套接字创建过程中常见的网络地址信息初始化方法如下:

1
2
3
4
5
6
7
struct sockaddr_in addr;
char* serv_ip = "211.217.168.13";
char* serv_port = "9190";
memset(&addr, 0, sizeof(addr)); // 结构体变量 addr 的所有成员初始化为0
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(serv_ip);
addr.sin_port = htons(atoi(serv_port)); // atoi 函数,将字符串类型的值转为整数型

而采用 INADDR_ANY 的方式,则可自动获取运行服务器端的计算机 IP 地址。

1
addr.sin_addr.s_addr = htonl(INADDR_ANY);

基于TCP的服务器端/客户端(1)

我们已经调用 bind 函数给套接字分配了地址和端口,接下来就要调用 listen 函数进入等待连接请求状态。

1
2
3
#include <sys/socket.h>

int listen(int sock, int backlog);

sock 文件描述符
backlog 连接请求等待队列的长度

1
2
3
#include <sys/socket.h>

int accept(int sock, struct sockaddr* addr, socklen_t* addrlen);

成功时返回创建的套接字文件描述符,失败时返回 -1。

该函数将自动创建一个新的套接字,并连接到发起请求的客户端。新的套接字用于数据 I/O。

而对于客户端来说,区别在于“请求连接”,可通过调用下面的函数发起连接请求。

1
2
3
#include <sys/socket.h>

int connect(int sock, struct sockaddr* servaddr, socklen_t addrlen);

成功时返回 0,失败返回 -1

Hello world 服务器端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
int main(int argc, char* argv[])
{
int serv_sock;
int clnt_sock;

struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
socklen_t clnt_addr_size;

char message[] = "Hello World!";

if(argc != 2)
{
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}

serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));

if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("bind() error");

if(listen(serv_sock, 5) == -1)
error_handling("listen() error");

clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
if(clnt_sock == -1)
error_handling("accept() error");

write(clnt_sock, message, sizeof(message));
close(clnt_sock);
close(serv_sock);
return 0;
}

void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

Hello world 客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
int main(int argc, char* argv[])
{
int sock;

struct sockaddr_in serv_addr;
char message[30];
int str_len;

if(argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}

serv_sock = socket(PF_INET, SOCK_STREAM, 0);
if(serv_sock == -1)
error_handling("socket() error");

memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));

if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
error_handling("connect() error");

str_len = read(sock, message, sizeof(message)-1);

if(str_len == -1)
error_handling("read() error");

printf("Message from server: %s \n", message);
close(sock);
return 0;
}

void error_handling(char* message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}

函数调用关系:

注意,客户端只能等到服务器端调用 listen 函数后才能调 connect 函数。同时要清楚,客户端调用 connect 函数前,服务器端可能率先调用 accept 函数。当然,此时服务器端在调用 accept 函数时进入阻塞状态,直到客户端调 connect 函数为止。