Linux网络编程 socket套接字
1.什么是socket套接字
Socket套接字由远景研究规划局(Advanced Research Projects Agency, ARPA)资助加里福尼亚大学伯克利分校的一个研究组研发。其目的是将TCP/IP协议相关软件移植到UNIX类系统中。设计者开发了一个接口,以便应用程序能简单地调用该接口通信。这个接口不断完善,最终形成了Socket套接字。Linux系统采用了Socket套接字,因此,Socket接口就被广泛使用,到现在已经成为事实上的标准。与套接字相关的函数被包含在头文件sys/socket.h中。
简单来说socket套接字就是一套TCP/IP通信协议的API。
套接字怎么使用呢?
套接字通信一般用于连接客户端和服务器。在服务器端:
由于服务器时被动提供服务,客户端主动连接,所以服务器应该先于客户端启动,启动服务器之后服务器的IP和端口不能变化。服务器的ip和端口要在启动之前绑定。
在客户端:
因为客户端是主动连接服务器,所以需要知道服务器的ip地址,和服务器主机上的服务器进程:端口。
到这我们先不着急看代码,还有一个重要问题需要解决:
首先我们想象一个通信的场景:
客户端给服务器发送了一个复杂的数据-结构体,在客户端数据的低字节存储在内存的低地址位,高字节存储在高地址位。而服务端恰好相反。这时候服务器接收的数据就会混乱。
2.字节序
在各种计算机体系结构中,对于字节、字等的存储机制有所不同,因而引发了计算机通信领域中一个很重要的问题,即通信双方交流的信息单元(比特、字节、字、双字等等)应该以什么样的顺序进行传送。如果不达成一致的规则,通信双方将无法进行正确的编/译码从而导致通信失败。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序(一个字节的数据当然就无需谈顺序的问题了)。
目前在各种体系的计算机中通常采用的字节存储机制主要有两种:Big-Endian 和 Little-Endian,下面先从字节序说起。
概念:
小端存储->主机字节序
存储方式:数据低字节存放在内存低地址位,高字节存放在高地址位(低低,高高)。我们的电脑都是按照小端存储在内存中存数据的。
大端存储->网络字节序
存储方式:数据的低字节存放在内存的高地址位,高字节存放在低地址位(低高,高低)。通信过程中使用的数据全部使用网络字节序。
举例:
// 有一个16位的数: 0x12345678 -> 这个数是 4 个字节
// 这个数在内存中按照大端, 小端如何存储
// 内存低地址位 --------------> 内存的高地址位
小端: 0x78 0x56 0x34 0x12
大端: 0x12 0x34 0x56 0x78
相关函数:
BSD Socket提供了封装好的转换接口,方便程序员使用。包括从主机字节序到网络字节序的转换函数:htons、htonl;从网络字节序到主机字节序的转换函数:ntohs、ntohl。
函数看这里:
#include <arpa/inet.h>
// h -> host -> 主机字节序
// to -> 转换
// n -> net -> 网络字节序
// s -> short -> 16位整形数
// l -> long -> 32位整形数
// 参数: 要转换的数据
// 返回值: 转换完成之后得到的结果
//一般端口的转换:
//主机字节序->网络字节序
uint16_t htons(uint16_t hostshort);
//网络字节序转主机字节序
uint16_t htons(uint16_t netshort);
//一般对整形IP进行转换
//主机字节序->网络字节序
uint32_t htonl(uint32_t hostlong);
//网络字节序->主机字节序
uint32_t ntohl(uint32_t netlong);
点分十进制IP地址转换
#include <arpa/inet.h>
// p -> 主机字节序的点分十进制IP地址(字符串): 192.168.1.100
// n -> 网络字节序的整形IP地址
// 将本地IP字符串 -> 整形大端IP地址
int inet_pton(int af,const char* src,void *dst);
参数:
-af:地址族协议 ipv4:AF_INET, ipv6:AF_INET6
-src: 要被转换的点分十进制的IP地址: 192.168.1.100这种格式
- dst: 转换得到的大端IP存储到这个指针指向的内存中
返回值:
转换成功: 0
失败: -1
// 整形大端IP地址 -> 将本地IP字符串
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
参数:
- af: 地址族协议, 使用的ipv4还是ipv6的ip地址
- ipv4: AF_INET
- ipv6: AF_INET6
- src: 这个指针指向的内存中存储了大端的整形IP地址
- dst: 存储得到的IP地址字符串
- size: dst参数指向的内存的总大小(容量)
返回值:
成功: 返回第三个参数dst的地址
失败: NULL
3.sockaddr数据结构
说完了字节序,我们来讨论一下用于存放地址信息的结构体: sockaddr
如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-r5Mv2QP7-1576504082585)(assets/sockaddr.png)]
//结构不算难理解,后面主要会用到sockaddr_in和sockaddr
struct sockaddr {
sa_family_t sa_family; // 地址族协议, ipv4, ipv6
char sa_data[14];
}
struct in_addr
{
in_addr_t s_addr;
};
struct sockaddr_in
{
sa_family_t sin_family; /* __SOCKADDR_COMMON(sin_) */
in_port_t sin_port; /* Port number. */
struct in_addr sin_addr; /* Internet address. */
/* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[sizeof (struct sockaddr) - __SOCKADDR_COMMON_SIZE -
sizeof (in_port_t) - sizeof (struct in_addr)];
};
4.套接字函数
#include <arpa/inet.h>
// 创建套接字
int socket(int domain, int type, int protocol);
参数:
- domain: 指定地址族协议
- AF_UNIX, AF_LOCAL: 用于本地进程间通信(本地套接字)
- AF_INET: 使用IPv4
- AF_INET6: 使用IPv6
- type: 使用什么传输协议
- SOCK_STREAM: 流式传输协议
- SOCK_DGRAM: 报式传输协议
- protocol: 指定具体的协议, 这个参数一般指定为0
- 使用流式协议中的tcp协议
- 使用报式协议中的udp协议
返回值:
成功: 返回用于socket的文件描述符
失败: -1
// 将监听的套接字和本地的IP和端口绑定
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 用于监听的套接字
- addr: 在这个变量中初始化要绑定的IP和端口信息
- addrlen: 第二个参数addr对应的内存大小
返回值:
成功: 0
失败: -1
// 设置监听->给监听的套接字
int listen(int sockfd, int backlog);
参数:
- sockfd: 绑定成功的套接字
- backlog: 可以最大同时监听多少个客户端连接
- 这个值最大是128, 如果参数值超过128, 还是按128处理
返回值:
成功: 0
失败: -1
// 等待并接受客户端连接, 阻塞函数
// 开始监听之后, 没有客户端连接服务器, 这时候程序就阻塞在了accept函数上
// 当检测到了客户端连接, 这个函数解除阻塞, 和客户端建立连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
参数:
- sockfd: 用户监听的套接字
- addr: 传出参数, 得到连接的客户端的IP和端口信息
- addrlen: 传入传出参数, 记录了参数addr指针对应的内存大小
返回值:
成功: 通信的文件描述符
失败: -1
// 接收数据
ssize_t read(int fd, void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
参数:
- 前三个参数和read一样
- flags: 套接字是一些属性, 默认使用0
// 发送数据
ssize_t write(int fd, const void *buf, size_t count);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
参数:
- 前三个参数和write一样
- flags: 套接字是一些属性, 默认使用0
// 连接服务器, 这个函数也是阻塞函数
// 连接完成之后, 解除阻塞
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
参数:
- sockfd: 客户端用于通信的套接字
- addr: 初始化服务器的IP和端口, 通过这个地址连接服务器
- addrlen: 参数 addr 指针指向的内存大小, sizeof(addr)
返回值:
成功: 0
失败: -1
通信代码如下:
//server端
#include <cstdio>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//1.创建套接字
int mfd = socket(AF_INET, SOCK_STREAM, 0);
if (mfd<0)
{
perror("socket");
exit(0);
}
//2.绑定IP和端口
struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_port = htons(8888);//本机字节序转网络字节序 (转为大端)
server.sin_addr.s_addr = INADDR_ANY;//使用这个宏绑定网卡所有ip
int ret = bind(mfd, (sockaddr*)& server, sizeof(server));
if (ret<0)
{
perror("bind");
exit(0);
}
//3.监听
ret = listen(mfd, 100);
if (ret<0)
{
perror("lfd");
exit(0);
}
//4.等待连接
//连接后获得通信文件描述符lfd,对应信息存放在sockaddr_in这个结构体中
struct sockaddr_in conaddr;
int len = sizeof(conaddr);
int lfd = accept(mfd, (struct sockaddr*)&conaddr, &len);
if (lfd<0)
{
perror("accept");
exit(0);
}
//5.通信
while (1)
{
char buf[100] = { 0 };
int count = recv(lfd, buf, sizeof(buf), 0);
printf("在服务器端打印%s\n", buf);
//发送数据,接收什么发送什么
send(lfd, buf, count, 0);
}
close(mfd);
close(lfd);
return 0;
}
//client端
#include<stdlib.h>
#include<sys/socket.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<iostream>
int main()
{
//1.创建套接字
int mfd = socket(AF_INET, SOCK_STREAM, 0);
if (mfd<0)
{
perror("socket");
exit(0);
}
//2.连接服务器使用服务器绑定的IP和端口
struct sockaddr_in client;
client.sin_family = AF_INET;
client.sin_port = htonl(8888);
//点分十进制转大端整形
inet_pton(AF_INET, "192.168.xxx.xxx",&client.sin_addr.s_addr);
int ret = connect(mfd, (sockaddr*)& client, sizeof(client));
if (ret<0)
{
perror("connect");
exit(0);
}
//3.通信
while (1)
{
char buf[1024] = { 0 };
std::cin >> buf;
send(mfd, buf, sizeof(buf), 0);//发送数据
//接收数据
char rebuf[1024] = { 0 };
int ret=recv(mfd, rebuf, sizeof(rebuf), 0);
if (ret==0)
{
printf("server disconnect...\n");
break;
}
printf("from server:%s\n", rebuf);
sleep(1);
}
close(mfd);
return 0;
}