Linux Socket编程
在网络中,IP 标识了全网唯一的主机,port 标识了主机上唯一的进程。socket = IP + port ,所以 socket 编程(套接字编程),就是通过用户的提供 IP 和端口,调用 socket 接口进行网络连接的技术。我们所进行的网络编程,实际就是在应用层上自定义应用层协议,应用层的下一层是传输层,所以 socket 编程需要我们了解 UDP 和 TCP 的区别和作用。其他层的协议,并不在应用层上不会进行操作。
在进行网络连接时,我们会在 UDP 和 TCP 两种连接方式中进行选择。UDP 的特点是:无连接,不可靠,面向数据报,适用于简单的网络传输中;TCP 的特点是:有连接,可靠,面向字节流,适用于需要保持稳定连接的网络传输中。
UDP 就像是赛博送信,信什么时候送到,以及能不能送到,UDP 都不关心。而 TCP 则相当于电话聊天,我们会先询问对方是否在听,再进行电话交流,最后还有告诉对方我要挂了(即 TCP 的三次握手和四次挥手)。
注意 UDP 不会对数据进行分包,也就是要传输一个特别大的文件时,适用 UDP 会导致超过 UDP 传输上限的后续内容全部丢失。如果非得适用 UDP 进行大文件传输,需要在应用层自定义 UDP 分包合包协议。
1. 网络编程常见API
1.1 socket常见API
#include <sys/types.h>
#include <sys/socket.h>
创建socket文件描述符(TCP/UDP,客户端+服务器)
int socket(int domain, int type, int protocol)
domain
:表示参数是一个域,需要输入预设好的宏变量。域的宏变量有很多,如果是AF_UNIX
则用于本地通信;如果是AF_INET
则用于网络通信。
type
:表示套接字的类型,需要输入预设好的宏变量。类型的宏变量也有很多,如SOCK_DGRAM
表示无连接、不可靠、指定大小的通信类型。
protocol
:表示指定协议,如果不想(不用)指定,可以设置为0。
return val
:成功返回值是一个文件描述符。网卡也是一个文件,socket()
相当于打开了网卡的文件,网络通信是再向网卡进行IO。
绑定端口号(TCP/UDP,服务器)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
bind()
用于将一本地地址(*addr
)与一套接口捆绑(sockfd
),该函数须在connect()
或listen()
调用前使用。
sockfd
:填入socket()
返回的文件描述符。
*addr
:是指向sockaddr_in
结构体或sockaddr_un
结构体的指针。这两个结构体都需要提前实例化进行设置才能使用。
addrlen
:表示addr
结构的长度,可以用sizeof操作符获得。
开始监听socket(TCP,服务器)
int listen(int sockfd, int backlog)
listen()
将sockfd
设置为监听套接字,此后便可以用accept()
接受连接。
sockfd
:填入socket()
返回的文件描述符。
backlog
:连接请求队列的最大长度(一般由2到4),用SOMAXCONN则为系统给出的最大值。
接收请求(TCP,服务器)
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
accept()
相当于一个中转站,sockfd
将来自网络另一端的连接接收,accept的返回值才是为该链接提供服务的 fd 。如果sockfd
无连接,accept()
将会阻塞等待,直到有链接。(注意sockfd
必须是调用listen()
之后的监听套接字)
return fd
:返回值是一个文件描述符,是用于为用户提供网络IO服务的套接字。
建立连接(TCP,客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen)
connect()
与bind()
的作用相同,只是connect()
在客户端中使用,bind()
在服务器中使用。
1.2 序列转换常用API
头文件 和 API:
#include <arpa/inet.h>uint32_t htonl(uint32_t hostlong);uint16_t htons(uint16_t hostshort);uint32_t ntohl(uint32_t netlong);uint16_t ntohs(uint16_t netshort);
这些函数将整型在主机字节序和网络字节序之间转换。h 表示 host ,n 表示 network ,l 表示 32 位长整数,s 表示 16 位短整数。
如果主机是小端字节序,这些函数将参数进行相应的大小端转换后返回。
如果主机是大端字节序,调用这些函数将不对参数做处理直接返回。
头文件:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
API:
int inet_aton(const char *cp, struct in_addr *inp)
char *inet_ntoa(struct in_addr in)
inet_ntoa()
将网络地址转换成“点分十进制”的字符串格式IP形式返回。
inet_ntoa()
返回了一个 char* ,显然在函数内部为我们申请了一块内存空间来保存 IP。man 手册上描述,返回值放到了静态存储区,不需要手动释放。重复调用(同一线程)会覆盖原来的内存空间。
int inet_pton(int af, const char *src, void *dst)
inet_pton()
能够将“点分十进制”字符串形式的IP传入,将其转为网络序列的4字节整数,这是一个方便且安全的转换接口。
*dst
:传入struct in_addr
的地址。
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size)
inet_ntop()
能够将网络序列的4字节整数IP转为主机序列后再转为“点分十进制”字符串形式返回,这是一个方便且安全的转换接口。
*dst
:传入struct in_addr
的地址。
1.3 网络通信发送接收常用API
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen)
recvfrom()
用于接收远程主机经指定的socket传来的数据,并把数据传到由参数buf指向的内存空间。适用于未连接的套接字,一般用于UDP。
ssize_t
:表示signed size_t类型,ssize_t数据类型用来表示可以被执行读写操作的数据块的大小。(ssize_t 相当于 long int,用这个名字为的是提高代码的自说明性)
len
:为可接收数据的最大长度。
falgs
:表示调用操作方式。一般设置为0。
socklen_t
:socklen_t是一个大小必须为16位的数据结构。(socklen_t 相当于int ,用这个名字为的是提高代码的自说明性)
*addrlen
:指向src_add
的指针,在调用recvfrom()
之前应设置为src_add
的长度,在调用之后将被设置为新接收到的地址的实际大小。
return val
:成功返回接收到的字符数,返回0表示对方关闭连接,失败返回-1。
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
recv()
适用于已连接的套接字,一般用于TCP。
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen)
sendto()
,指向一指定目的地发送数据,用于发送未建立连接的UDP数据包 (参数为SOCK_DGRAM)。参数flags 一般设0。其他说明略。
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
send()
用于TCP。
FILE *popen(const char *command, const char *type)
popen()
通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。
*command
:Linux命令
*type
:即打开文件的方式,如果 type 是 “r” 则文件指针连接到 command 的标准输出;如果 type 是 “w” 则文件指针连接到 command 的标准输入。
return val
:成功返回一个管道的文件指针,失败返回NULL。
1.4 sockaddr结构
socket API 是一层抽象的网络编程接口,适用于各种底层网络协议,但各种网络协议的地址格式并不相同。于是就有了 sockaddr_in 结构体用于本地通信,sockeaddr_un结构体用于网络通信,但 socke API 没有各自为其设置不同的接口,而是使用指针作为参数,通过指针指向不同的结构体类型实现了多态。
1.5 sockaddr_in
struct sockaddr_in
是一个在 C 和 C++ 中用于描述 IPv4 地址的结构体。这个结构体通常用于套接字编程中,表示一个 IPv4 地址和端口号。struct sockaddr_in
结构体的大小通常是 16 个字节,其中 sin_zero
的存在确保了与 sockaddr
结构体的对齐和大小一致性。
注意:为保证使用中不被随机值影响,sockaddr_in对象在定义后一般会使用memset()将内部随机值设置为0。
sockaddr_in
有四个成员变量:
sin_family
sin_family指定了协议族,在sockaddr_in中只能被定义成
AF_INET
(表示 IPv4 使用的地址族)
sin_port
sin_port用于存储端口号。sin_port以网络字节序存储(大端),所以在设置时需要将主机序列转为网络序列(使用
htons()
)
sin_addr
sin_addr是一个结构体类型,其包含的
s_addr
成员表示32位IPv4地址,以网络字节序存储,使用inet_addr()
或inet_pton()
直接存储IP即可。sin.sin_addr.s_addr = inet_addr("192.168.1.1"); // 将字符串形式的IP地址转换为网络字节序并赋值 inet_pton(AF_INET, "192.168.1.1", &sin.sin_addr); // 使用 inet_pton() 函数
注意:将s_addr设置为
INADDR_ANY
表示进行任意IP地址绑定。 在使用云服务器时对其设置为INADDR_ANY才可在本地测试网络通信。
sin_zero
这个成员仅用于保证sockaddr_in的结构体大小为16位。
2. socket UDP编程和TCP编程
UDP
UDP的特点是无连接,不可靠,面向数据报。与TCP相比,UDP较简单。UDP的 socket 在创建时,要将 type
参数设置为 SOCK_DGRAM
表示面向数据报,使用 recvfrom()
接收彼端传来的数据,使用 sendto()
向彼端发送数据。
TCP
TCP的特点是有连接,可靠,面向字节流。与UDP相比,TCP较复杂。TCP的 socket 在创建时,要将 type
参数设置为 SOCK_STREAM
表示面向字节流,由于TCP有连接,套接字在使用前需要使用 listen()
将套接字设置为监听套接字,再使用 accept()
接收请求。在客户端,要使用 connect()
使客户端与服务器连接。使用 recv()
接收彼端的数据,使用 send()
向彼端发送数据。
共同点
- 端口号的类型必须是
uint16_t
,使用端口号前注意当前是网络序列还是主机序列,是否需要进行序列转换。 - UDP和TCP的服务器端的IP需要设置为
ADDR_ANY
,表示随机IP地址,只需要确定端口号。而客户端则需要确定IP地址和端口号。 - UDP和TCP在外层创建的socket不需要显式的bind,但是一定要有自己的 IP 和 port,所以需要隐式的 bind,OS会使用自己的 IP 和随机的端口号,自动 bind socket。
- 网络编程中,通常
read()
返回值大于0
认为是正常读取,为0
认为是通信正常结束,小于0
认为是出错。
3. windows作为client访问Linux
Windows没有 Linux 使用的那些系统标准库,要使用相同的函数,需要手动引入:
#include <WinSock2.h>
#include <windows.h>
#pragma comment(lib,"ws2_32.lib") //这个和WinSock2.h是对应关系
#pragma warning(disable : 4996) //用于屏蔽c语言接口不安全的报错
在Windows中,使用 ws2_32.lib
的哪一个版本,也需要声明:
int main()
{WSADATA wsd; //WSA是Windows Sockets API的缩写WSAStartup(MAKEWORD(2,2),&wsd); //表示使用2.2版本的库return 0;
}
Windows中还对sockfd进行了封装:
SOCKET sockfd = socket(AF_INET,SOCK_DGRAM,0); //SOCKET是套接字描述符,实际用int也能编过
closesocket(sockfd); //关闭sockfd也有对应的函数
因为Windows没有一切皆文件的概念,所以套接字会有一个描述符类型
在使用完创建的套接字后,还需要对加载的库资源进行清理:
WSACleanup();
4. 开发端口
测试程序:TCPdemo
测试工具:Windows cmd
命令:telnet [ip] [port]