Socket编程(一)——基础函数

本篇笔记基于尹圣雨的《TCP/IP编程》,主要记录Socket编程中比较关键的一些知识点和C函数。本笔记会像书中一样同时介绍Windows和Linux两个操作系统下的Socket函数,特别是两个操作系统下操作不同的地方。

套接字通信基本流程

套接字概念

套接字(socket)实际上就是网络数据传输用的软件设备,与其他用户通信的数据都需要通过这个软件接口进行传输。在代码中,它以整型变量的形式存在,实际上是由操作系统管理的。Linux操作系统把套接字和文件描述符看做是一个东西,所以对文件操作的函数大部分也可以对套接字使用,而windows认为套接字独立于文件描述符。

服务端方面

只考虑最简单的一对一通信情况,服务端需要建立好套接字,将套接字与本机的一个端口绑定,然后监听套接字是否被请求连接,如果收到连接请求,就接受连接请求并读取数据。

客户端方面

客户端与服务端对比较为简单,只需要向某主机的一个端口发起连接请求,待被接受后发送数据。

Linux下的Socket基本函数

1
2
3
4
5
6
7
8
9
10
11
#include<sys/socket.h>
int socket(int domain, int type, int protocol);

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);

int connet(int sockfd, struct sockaddr *serv_addr, socklen_t addrlen);

由于Linux下对socket的操作与对文件无异,所以这里再介绍一下Linux下的文件操作函数。

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
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
/*
* 打开path路径的文件,flag是常量,有O_CREATE,O_TRUNC,O_APPEND,O_RDONLY,O_WRONLY,O_RDWR。
* 成功返回文件描述符,失败返回-1
*/
int open(const char *path,int flag);

#include<unistd.h>
/*
* 关闭某个文件描述符,成功返回0,失败返回-1
*/
int close(int fd);

/*
* fd:文件描述符,buf:缓冲区地址,nbytes:要传输的字节数
* 成功时返回写入的字节数,失败时返回-1
*/
ssize_t write(int fd, const void * buf, size_t nbytes);

/*
* 与write类似,不再赘述
*/
ssize_t read(int fd, void * buf, size_t nbytes);

Windows下的socket基本函数

环境准备

为了在Windows下开发网络程序,需要包含头文件winsock2.h并添加依赖项,即链接ws2_32.lib库。

函数介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<winsock2.h>
int WSAStartup(WORD wVersionRequested, LPWSADATA lpwSAData);

int WSACleanup(void);

SOCKET socket(int af, int type, int protocol);

int bind(SOCKET s, const struct sockaddr * name, int namelen);

int listen(SOCKET s, int backlog);

SOCKET accept(SOCKET s, sturct sockaddr * addr, int * addrlen);

int connect(SOCKET s, const struct sockaddr * name, int namelen);

int closesocket(SOCKET s);

WSAStartup

该函数第一个参数是需要的Winsock版本信息,通常用MAKEWORD宏函数构造一个版本号。第二个参数填入WSADATA类型变量的地址,调用完该函数后,WSADATA会填充初始化的库信息。

其余函数的使用方法都与Linux下大同小异,只是宏变量有所区别。

数据传输

Windows中的套接字返回的不是文件描述符而是句柄(handle),所以并不能直接用操作文件的函数来操作套接字。故引入以下两个新的函数:

1
2
3
4
5
6
/*
* len:接受的最大字节数,flags:接受数据时用到的多种选项,通常写0
*/
int send(SOCKET s, const char * buf, int len, int flags);

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

套接字类型与协议设置

本小节介绍在创建套接字时使用的三个参数,在两个平台下的操作是相同的。

协议类型

创建socket的函数中第一个要求我们指定使用的协议族,协议族包含以下几种:

名称协议族PF_INETIPv4互联网协议族PF_INET6IPv6互联网协议族PF_LOCAL本地通信的UNIX协议族PF_PACKET底层套接字的协议族PF_IPXIPXNovell协议族\begin{array}{|c|c|} \hline 名称 & 协议族 \\ \hline PF\_INET & IPv4互联网协议族 \\ \hline PF\_INET6 & IPv6互联网协议族 \\ \hline PF\_LOCAL & 本地通信的UNIX协议族 \\ \hline PF\_PACKET & 底层套接字的协议族 \\ \hline PF\_IPX & IPX Novell协议族 \\ \hline \end{array}

由于IPv6协议族还未普及,所以大部分情况下我们使用IPv4协议族。

套接字类型

面向连接的套接字(SOCK_STREAM)

特点是没有数据边界、按序传输和保证交付。由于没有数据边界这个特点,在使用该类型套接字时通常需要自己设计协议。

面向消息的套接字(SOCK_DGRAM)

该类型的套接字有以下特点:不保证交付、限制数据大小、有数据边界,虽然听起来不怎么靠谱,但是这种套接字有个非常大的优势,就是速度快。

最终选择的协议

第三个参数令人疑惑,很多人以为“套接字类型”这个参数就指定了使用的协议,但实际上并不是。只是在IPv4协议族下只有TCP这一个面向连接的套接字,有可能遇到“同一个协议族中存在多个数据传输方式相同的协议”。

对于TCP协议,该参数填写IPPROTO_TCP;对于UDP协议,该参数填写IPPROTO_UDP

地址族与数据序列

绑定地址信息

在使用socket通信时,通常需要同时绑定地址和端口号,这里仅介绍绑定IPv4的地址的方法。需要先声明一个sockaddr_in 类型的结构体,其原型如下:

1
2
3
4
5
6
7
struct sockaddr_in
{
sa_family_t sin_family; // Address Family
uint16_t sin_port; // port number
struct in_addr sin_addr; // IP address of 32 bit
char sin_zero[8]; // no use
}
  • sin_family通常填AF_INET

  • sin_port需要按网络字节保存

  • sin_addr也以网络字节序保存

  • sin_zero填充0,只是为了与sockaddr类型大小相同。

sockaddr是通用的地址,而sockaddr_in是专为IPv4协议设计的,故大小存在差异。

字节序转换

由于数据在不同CPU体系架构下的保存方式不同,所以需要统一转换微网络序(即大端序)。可以使用以下四个函数:

1
2
3
4
unsigned short htons(unsigned short); // host to network (short)
unsigned short ntohs(unsigned short); // network to host (short)
unsigned long htonl(unsigned long); // host to network (long)
unsigned long ntohl(unsigned long); //network to host (long)

虽然有些主机的数据储存方式本身为大端序,但是为了程序的可移植性,还是需要在程序中加入此语句。

初始化地址

点分十进制法表示的地址转换为整型是十分棘手的,幸运的是socket库中自带了相关的转换函数;

1
2
3
4
5
6
#include <arpr/inet.h>
in_addr_t inet_addr(const char * string); // 成功时返回网络字节序的整型地址,失败时返回INADDR_NONE

int inet_aton(const char * string, struct in_addr *addr); //成功返回1,失败返回0,第二个参数是希望赋值的变量

char * inet_ntoa(struct in_addr adr); // 成功时返回转化的字符串地址值,失败时返回-1

以上便是socket的基本函数以及创建socket套接字的主要流程。


Socket编程(一)——基础函数
http://zhouhf.top/2023/03/10/Socket编程(一)——基础函数/
作者
周洪锋
发布于
2023年3月10日
许可协议