网络套接字socket
参考:李慧琴老师视频 + 笔记: Linux系统编程学习笔记
仅作自己学习笔记使用,侵删
引言
上一章考察了各种UNIX系统所提供的经典进程间通信机制(IPC):管道、FIFO、消息队列、信号量以及共享存储。这些机制允许在同一台计算机上运行的进程可以相互通信。本章将考察**不同计算机(通过网络相连)**上的进程相互通信的机制:网络进程间通信(network IPC)。
在本章中,我们将描述套接字网络进程间通信接口,进程用该接口能够和其他进程通信,无论它们是在同一台计算机上还是在不同的计算机上。实际上,这正是套接字接口的设计目标之一:同样的接口既可以用于计算机间通信,也可以用于计算机内通信。尽管套接口可以采用许多不同的网络协议进行通信,但本章的讨论限制在因特网事实上的通信标准:TCP/IP协议栈。
协议的制定(讨论:跨主机传输需要注意的问题)
1.字节序问题
大端:低地址处存放高字节
小端:低地址处存放低字节(x86)
不管是文件传输,还是io实现,永远是低地址处的数据先出去
所以接收端如果接收到大端数据,按小端去解析,数值差距很大
假设要存放的数据是 0x30313233,那么 33 是低位,30 是高位,(左高右低)。在大端存储格式中,30 存放在低位,33 存放在高位;而在小端存储格式中,33 存放在低位,30 存放在高位。
主机字节序:host
网络字节序:network
_ to _ _(长度,要么s 两个字节16位,要么l 四个字节32位): ntohl, ntohs, htons, htonl
2.对齐
在32位的机器上,各占用4,4,1共9个字节的大小。但是编译器会将其自动对齐,此时为12个字节。
struct
{
int i;
float f;
char ch;
}
对齐理解:如果当前的起始地址号能够整除这个成员的sizeof
对齐原因:
- 平台原因: 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:结构体的内存对齐是拿空间来换取时间的做法。优点是提高了可移植性和cpu性能。
解决办法:不对齐
网络传输的结构体中的成员都是紧凑的,所以不能地址对齐,需要在结构体外面增加 attribute((packed))。例如:
struct msg_st {
uint8_t name[NAMESIZE];
uint32_t math;
uint32_t chinese;
}__attribute__((packed));
3.类型长度问题
标准C并没有对int、char这样的基本数据类型占用多大字节做一个明确的规定,例如:
一个16位的机器上,int可能占2个字节;
一个32位的机器上,int可能占4个字节;
解决:使用int32_t
、uint32_t
、int64_t
、int8_t
、uint8_t
等类型明确指定占用的位数。这些类型包含在头文件<stdint.h>中。
SOKECT是什么
套接字是一种通信机制(通信的两方的一种约定),socket屏蔽了各个协议的通信细节,提供了tcp/ip协议的抽象,对外提供了一套接口,通过这个接口就可以统一、方便的使用tcp/ip协议的功能。这使得程序员无需关注协议本身,直接使用socket提供的接口来进行互联的不同主机间的进程的通信。我们可以用套接字中的相关函数来完成通信过程。
1.套接字描述符
套接字是通信端点的抽象。正如使用文件描述符访问文件,应用程序用套接字描述符访问套接字。套接字描述符在UNIX系统中被当作是一种文件描述符。事实上,许多处理文件描述符的函数(如read和write)可以用于处理套接字描述符。
为创建一个套接字,调用socket函数:
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
我要用domain协议族中的protocol协议,实现type类型的传输
domain
:域,或者协议族,确定通信的特性。即采用什么协议来传输数据。AF_UNIX、AF_LOCAL
:本地协议;AF_INET
:IPV4 协议;AF_INET6
:IPV6 协议;AF_IPX
:是非常古老的操作系统,出现在 TCP/IP 之前AF_NETLINK
:是用户态与内核态通信的协议;AF_APPLETALK
:苹果使用的一个局域网协议;AF_PACKET
:底层 socket所用到的协议,比如抓包器所遵循的协议一定要在网卡驱动层,而不能在应用层,否则无法见到包封装的过程。再比如 ping 命令,想要实现ping 命令就需要了解这个协议族。
type
:确定套接字的类型,进一步确定通信特征。SOCK_STREAM
:流式套接字,特点是有序、可靠。有序、双工、基于连接的、以字节流为单位的。- 可靠不是指不丢包,而是流式套接字保证只要你能接收到这个包,那么包中的数据的完整性一定是正确的。
- 双工是指双方都能收发。
- 基于连接的是指:通信双方是知道对方是谁。
- 字节流是指数据没有明显的界限,一端数据可以分为任意多个包发送。
SOCK_DGRAM
:报式套接字(数据报套接字),比如结构体,无连接的,固定的最大长度,不可靠的消息。SOCK_SEQPACKET
:提供有序、可靠、双向、基于连接的数据报通信。(类似消息队列)SOCK_RAW
:原始的套接字,提供的是网络协议层的访问。SOCK_RDM
:数据层的访问,不保证传输顺序。
protocol
:具体使用哪个协议。在 domain 的协议族中每一个对应的 type 都有一个或多个协议,使用协议族中默认的协议可以填写0。返回值
:如果成功,返回套接字描述符;如果失败,返回 -1,并设置 errno。
调用socket与调用open相类似。在两种情况下,均可获得用于I/O的文件描述符。当不再需要该文件描述符时,调用close来关闭对文件或套接字的访问,并且释放该描述符以便重新使用。
2.主动端和被动端(以下均以报式套接字为例)
主动端
:
1.取得socket
2.给socket关联绑定地址(可以省略)
3.发/收消息
4.关闭socket
被动端
:
1.取得socket
2.给socket关联绑定地址
3.收/发消息
4.关闭socket
为什么主动端绑定地址可以省略?
主动端可省略,不必与操作系统约定端口,由操作系统指定随机端口。
3.寻址
标识目标通信进程需要网络地址(IP)和端口号(port),前者标识网络上想与之通信的计算机,后者帮助标识特定的进程,因此需要将套接字与这两者进行绑定关联。
补充知识点:void*
- void*类型的指针其实本质就是一个过渡型的指针状态,必须要**赋予类型(强制类型转换)**才能正常使用。
void *的范围较大,所以强制转换,使其进行范围缩小。
- 只能单向类型转换。void可以转化成其他类型,但是有类型的不能转化成void。
任何类型的指针都可以直接赋值给void* 型指针,无需进行强制类型转换,相当于void *包含了其他类型的指针。 - 在函数调用过程中的使用作为输入输出参数,表示可以接受任意类型的输入指针和输出任意类型的指针,可以灵活使用任意类型的指针,避免只能使用固定类型的指针。
3.1 地址格式
在用bind函数绑定地址时,不同的协议有不同的地址类型
NAME
bind - bind a name to a socket
SYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);
The actual structure passed for the addr argument will depend on the address family. The sockaddr structure is defined as something like:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
}
The only purpose of this structure is to cast the structure pointer passed in addr in order to avoid compiler warnings.
不同的协议族有不同的sockaddr, 以 AF_INET
为例:
struct sockaddr_in {
sa_family_t sin_family; /* address family: AF_INET */
in_port_t sin_port; /* port in network byte order */
struct in_addr sin_addr; /* internet address */
};
/* Internet address. */
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
在填写sin_addr
时,我们需要将点式地址转为uint_32类型地址,那么可以借助函数inet_pton
:
将点分十进制的文本字符串格式转换为网络字节序的地址(一个uint32_t的大整数),放在addr中
NAME
inet_pton - convert IPv4 and IPv6 addresses from text to binary form
SYNOPSIS
#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);
af
:协议族,仅支持以下两种:AF_INET
:IPV4 协议AF_INET6
:IPV6 协议
src
: 点分十进制的文本字符串dst
: 存放转换后网络字节序二进制的地址
那么主动端的寻址如下
#define PORT 1986;
sockadd_in laddr;
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(PORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(af, "0.0.0.0", &laddr.sin.addr.s_addr);
0.0.0.0还可以如下定义:
/* Address to accept any incoming messages. */
#define INADDR_ANY ((unit_32) 0x00000000)
绑定如下:
bind(sd, void(*)&laddr, sizeof(laddr));
4.接收和发送
函数参数的区别:
1.报式套接字
需要对每一条消息都记录是从哪来的,需要对端的地址和地址长度
2.流式套接字
已经建立好一对一的连接了,所以不需要对端地址和长度
4.1 接收函数
NAME
recv, recvfrom, recvmsg - receive a message from a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
以为例报式:从sockfd接收消息,接收的消息buf,长度len, 有没有特殊要求 ,对端的地址,和地址长度
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
4.2 发送函数
NAME
send, sendto, sendmsg - send a message on a socket
SYNOPSIS
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
以为例报式:sockfd发送消息,发送的消息buf,长度len, 有没有特殊要求, 对端的地址, 和地址长度。简单起见,我们可以将对端的地址通过传参传递
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
5.报式套接字示例
proto.h
# ifndef __PROTO_H_
# define __PROTO_H_
# define RECVPORT 1986
# define NAMESIZE 11
struct msg_t
{
//定长,不可能有负值
uint8_t name[NAMESIZE];
uint32_t chinese;
uint32_t math;
}__attribute__((packed));
# endif
rcver.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
#define IPSTRSIZE 64
int main() {
// 套接字描述符
int sd;
// laddr 本机地址
// raddr 对端地址
sockaddr_in laddr, raddr;
// 存储接收到的结构体
msg_t mssg;
// 存储对端地址,点分式
char ipstr[IPSTRSIZE];
// 创建socket,创建协议为ipv4的报式套接字,0为默认协议,即UDP
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
// 填写本机的地址信息
laddr.sin_family = AF_INET;
// ip地址和网络端口号是要通过网络发送过去的,所以需要考虑字节序的问题,也就是htons
laddr.sin_port = htons(RECVPORT);
// 因为本机的ip地址有可能会变化,为了避免ip地址每一次变化,都要进来修改,所以给它匹配一个万能地址0.0.0.0
// 对"0.0.0.0"的定义是any address.就是说在当前绑定阶段,本机的ip地址是多少,这四个0就会自动换成当前的ip地址.
inet_pton(AF_INET, "0.0.0.0", &laddr.sin_addr.s_addr);
//绑定接收的ip地址和端口号
if (bind(sd, (const sockaddr*)&laddr, sizeof(laddr)) < 0) {
perror("sendto()");
exit(1);
}
// 接收
// !!!!这里一定要初始化对端地址的大小!!!
socklen_t addr_len = sizeof(raddr);
while (1) {
if (recvfrom(sd, (void*)&mssg, sizeof(mssg), 0, (sockaddr*)&raddr, &addr_len) < 0) {
perror("recvfrom()");
exit(1);
}
inet_ntop(AF_INET, &raddr.sin_addr, ipstr, IPSTRSIZE);
std::cout << "---------recive message from " << std::string(ipstr) << ":" << ntohs(raddr.sin_port) << "---------" << std::endl;
// 单字节传输不涉及到大端小端的存储情况
std::cout << "name" << ":" << mssg.name << std::endl;
std::cout << "math" << ":" << ntohs(mssg.math) << std::endl;
std::cout << "chinese" << ":" << ntohs(mssg.chinese) << std::endl;
}
//关闭
close(sd);
exit(1);
}
运行编译好的代码,用新一个终端,使用命令netstat -anu查看,可以看到Local Address 0.0.0.0:1986
vratdrh7771.rsv.ven.veritas.com [66]: netstat -anu
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address Foreign Address State
udp 0 0 127.0.0.1:55212 127.0.0.1:55212 ESTABLISHED
udp 0 0 0.0.0.0:68 0.0.0.0:*
udp 0 0 0.0.0.0:111 0.0.0.0:*
udp 0 0 127.0.0.1:323 0.0.0.0:*
udp 0 0 0.0.0.0:752 0.0.0.0:*
udp 0 0 127.0.0.1:843 0.0.0.0:*
udp 0 0 0.0.0.0:42743 0.0.0.0:*
udp 0 0 0.0.0.0:1986 0.0.0.0:*
snder.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <string.h>
#include <arpa/inet.h>
#include "proto.h"
int main(int argc, char **argv) {
int sd;
msg_t sendmssg;
struct sockaddr_in raddr;
//创建socket
sd = socket(AF_INET, SOCK_DGRAM, 0);
if (sd < 0) {
perror("socker()");
exit(1);
}
//本地绑定(可以省略)
//填写发送消息
memset(&sendmssg, '\0', sizeof(sendmssg));
//strcpy(sendmssg.name, "tracy");
memcpy(sendmssg.name, "tracy", sizeof("tracy"));
sendmssg.chinese = ntohs(100);
sendmssg.math = ntohs(100);
//对端地址
raddr.sin_family = AF_INET;
raddr.sin_port = ntohs(RECVPORT);
inet_pton(AF_INET, argv[1], &raddr.sin_addr);
//发送
if (sendto(sd, &sendmssg, sizeof(sendmssg), 0, (const sockaddr*)&raddr, sizeof(raddr)) < 0) {
perror("sendto()");
exit(1);
}
//关闭
close(sd);
}
运行结果
vratdrh7771.rsv.ven.veritas.com [144]: ./rcver
---------recive message from 10.85.171.130:54485---------
name:tracy
math:100
chinese:100
---------recive message from 10.85.171.130:41376---------
name:tracy
math:100
chinese:100
vratdrh7771.rsv.ven.veritas.com [57]: ./snder 10.85.171.130