Linux Socket 编程

Linux socket编程

好多工作,唧唧歪歪的事情真多,本来打算今天早上写的,现在好了可以回家了(下班时间)。

主机字节序 网络字节序

主机字节序
主机字节序就是多字节数据中字节在内存中的存储顺序,有大端字节序和小端字节序。
大端/小端的语义就是多字节值的哪一端存储在该值的起始位置,大端存储在起始位置就是大端字节序(Big-Endian),小端存储在起始位置就是小端字节序(Little-Endian)。
如4Byte int 0x12345678小端存储方式:
31 -24 23-16 15-8 7-0
|0x12|0x34|0x56|0x78|
大端存储方式:
31 -24 23-16 15-8 7-0
|0x78|0x56|0x34|0x12|

网络字节序
网络中数据流的传输方式为字节流,传输一个多字节值时先传输哪个字节呢?
网络字节序定义,发送端发送的第一个字节为多字节值的高位,接收端接收到的第一个字节也作为多字节值的高位。但是发送端在发送数据时,发送的第一个字节为该数据在内存中起始位置对应的字节;同样接收端在接收到第一个字节时存储到该数据起始位置。也就是说不管是发送端还是接收端,数据在内存中的存储方式都应该和网络字节序一样为大端字节序。
但是不同的主机有不同的字节序,所以在网络应用程序中发送端发送数据前可以将主机序转换为网络字节序,接收端接收到数据后由网络字节序转换为主机序。
可以使用下面的库函数进行主机序与网络字节序转换:

#include <arpa/inet.h>
// h:host, n:network, l:long, s:short
// 如果字节序为小端字节序将转换为大端字节序,如果为大端字节序将不进行转换而直接返回数据(发送端/接收端都一样)
uint32_t htonl(uint32_t hostlong);
uint16_t htons(unit16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohl(uint16_t netshort);

什么是socket

在说socket之前,先了解下在本地以及网络中如何标识一个唯一的进程。
在本地中通过PID唯一标识一个进程;但是在网络中需要通过IP地址唯一标识一台主机,在通过协议+端口唯一标识主机中进程。(IP地址:网络层;协议+端口:传输层/应用层)
socket是网络中进程通信的接口,由于UNIX的思想为一切皆文件,socket可以理解为一种特殊的文件,其操作模式为open->read/wirte->close模式的特殊实现方式。

sockaddr sockaddr_in/sockaddr_in6 sockaddr_un

我们知道网络中标识唯一进程需要IP地址、协议、及端口号,而网络地址就是用来保存这些信息的,根据不同的协议有不同的地址结构体。

// IPv4
// sockaddr 通用套接字地址, 一般作为参数传递,如传入bind()
#include socket.h
typedef unsigned short int sa_family_t;
struct sockaddr {
    sa_family_t sa_family; // address family
    char sa_date[14]; // address data
}

// sockaddr_in internet环境套接字地址,一般用于初始化网络地址,然后转换为sockaddr传递给函数使用(sockaddr_in/sockaddr大小相同,可以相互转换)
#incldue netinet/in.h
typedef unsigned short int in_port_t;
struct sockaddr_in {
    sa_family_t sin_family; // address family,AF_INET
    in_port_t sin_port; // port munber
    struct in_addr sin_addr; // internet address
    unsigned char sin_zero[8]; // pad to size of sockaddr
}
typedef unsigned int in_addr_t;
struct in_addr {
    in_addr_t s_addr;
}
// UNIX域
#defien UNIX_PATH_MAX 108
struct sockaddr_un {
    sa_family_t sun_family; // address family, AF_LOCAL/AF_UNIX
    char sun_path[UNIX_PATH_MAX]; // pathname
}
// IPv6
struct sockaddr_in6 {
    sa_family_t sin6_family; // address family, AF_INET6
    in_port_t sin6_port; // port number
    uint32_t sin6_flowinfo; // IPv6 flow info
    struct in6_addr sin6_addr; // IPv6 address
    uint32_t sin6_scope_id; // Scope ID
}
struct in6_addr {
    unsigned char s6_addr[16];
}
// 地址转换函数
// IPv4 and IPv6
int inet_aton(const char *cp, struct in_addr *inp); // 点分十进制IP到二进制格式(网络字节序)
in_addr inet_addr(const char *cp); // 与inet_aton()相同,但不能传入255.255.255.255,否则返回INADDR_NONE
char *inet_ntoa(struct in_addr in); // 二进制格式(网络字节序)转换为点分十进制IP
// IPv6
int inet_pton(int af, const char *src, void *dest); // af:AF_INET/AF_INET6, 点分十进制到二进制格式(网络字节序,根据af转换为不同的格式4字节in_addr/16字节in6_addr,然后给dest)
const char *inet_ntop(int af, const void *src, char *det, socklen_t size); // 二进制格式到点分十进制,zise为det的长度,避免溢出

socket函数

1、int socket(int domain, int type, int protocol);
socket()函数用于打开一个socket,返回socket描述符,这个socket描述符用于后续的操作。如读写。
domain:协议域/协议簇,AF:Address Family,地址簇,BSD使用;PF:Protocol Family协议簇,POSIX使用。(Linux中使用AF或者PF前缀是一样的)
AF_INET :IPv4地址(32bit地址+16端口号);
AF_INET6:IPv6地址(128bit地址+16端口号);
AF_UNIX/AP_LOCAL:UNIX域(使用路径名作为地址);
AF_PACKET:底层数据包接口;
type:socket类型
SOCK_STRAEM:面向连接的流套接字,TCP协议;
SOCK_DGRAM:面向非连接的用户数据报套接字,UDP协议;
SOCK_RAW:原始套接字;
protocol:协议
IPPROTO_TCP:TCP协议;
IPPROTO_UDP:UDP协议;
IPPROTO_ICMP:ICMP协议;
ETH_P_IP:IP协议;
ETH_P_ARP:ARP协议;
ETH_P_RARP:RARP协议;
ETH_P_ALL:IP/ARP/RARP协议;

使用socket函数的三种形式
发送/接收的数据为应用层数据:
socket(AF_INET, SOCK_STREAM|SOCK_DGRAM, 0); // protocol为0,会根据type自动选择协议类型
原始套接字1:发送/接收的数据从网络层开始
socket(AF_INET, SOCK_ARW, IPPROTO_TCP | IPPROTO_UDP | IPPROTO_ICMP);
接收时,接收到包含IP报头的数据包;
发送时,只能发送包含TCP、UDP、或其他协议的报头,IP报头自动封装,设置socket的IP_HDRINCL可以手动发送IP报头(以太网报头自动封装);
原始套接字2:发送/接收的数据从链路层开始
socket(AF_PACKET, SOCK_RAW/SOCK_DGRAM, htonl(ETH_P_IP | ETH_P_ARP | ETH_P_RARP | ETH_P_ALL));
灰常强大的套接字,发送接收以太网数据包,只有root才可以创建这种套接字;
type:SOCK_RAW与SOCK_DGRAM的区别在于,SOCK_DGRAM发送接收的数据包不包括以太网数据报头;

// 将套接字描述符sockfd与sockaddr地址绑定
// 服务器启动时绑定一个地址(IP+端口)用于提供服务;客户端不需要绑定,在connect时由系统自动分配
2、int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// 监听套接字sockfd,backlog为连接队列最大数量
3、int listen(int sockfd, int backlog);

// 客户端与TCP服务器建立连接
// sockfd为客户端套接字描述符,sockaddr为服务器地址,socklen为地址长度
4、int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

// TCP服务器等待来自客户端的连接(会阻塞直到客户端产生连接),建立连接后就可以进行I/O操作(读写操作)
// sockfd为服务器监听的套接字描述符,addr指针保存客户端地址,addlen地址长度,返回内核生成的连接描述符(表示与客户端的连接)
5、int accept(int sockfd, const struct sockaddr *addr, socklen_t *addrlen);

// 当服务器与客户端建立连接后就可以进行I/O操作了
// 将buf的nbytes字节内容写入fd文件描述符,返回成功写入的字节数,error返回-1(errno)
6、ssize_t write(int fd, const void*buf, size_t nbytes);
// 网络程序中分两种情况处理:
返回值大于0,写入部分或者全部数字;
返回值小于0,error(errno=EINTR,写时中断;errno=EPIPE,网络连接中断,对方已关闭连接);

//从fd读取内容到buf(nbyte返回限定buf的大小防溢出),返回读到的字节数;返回0,读到EOF;返回值小于0,error(errno=EINTR,读时中断;errno=ECONNRES,网络连接中断)
7、ssize_t read(int fd, void *buf, size_t nbyte);

// recv/send与read/write相似,多了控制选项flag
8、ssize_t recv(int sockfd, void *buf, size_t len, int flags);
9、ssize_t send(int sockfd, void *buf, size_t len, int flags);
flags选项:
MSG_DONTROUTE:不查找路由表(send()函数使用,告诉IP协议,目的主机在本地网络上);
MSG_OOB:发送/接收外带数据;
MSG_PEEK:读取数据,并不从缓冲区移走数据(一般为多进程读取);
MSG_WAITALL:等待所有数据(recv函数使用,一直阻塞直到等待的条件满足,如指定读取一定的数据);

// recvfrom/sendto与recv/send相似,多了发送/接收的地址信息
10、ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); // 保存信息来源地址src_addr
11、ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); // 发送的目的地址dest_addr

// 发送/接收的地址信息和数据信息保存在msghdr
12、ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
13、ssize_t recvmsg(int sockfd, const struct msghdr *msg, int flags);

// 设置/获取套接字的选项
13、int setsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
14、int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t *optlen);
// level:控制套接字层次,SOL_SOCKET通用套接字、IPPROTO_IP(IP选项)、IPPROTO_TCP(TCP选项);
// optname:选项名称,如IP_HDRINCL(在数据报包中加入IP头部);
// optvalue:选项值,根据选项名称进行类型转换;
// optlen:optval的长度;

服务器模型

服务器模型是网络编程的思想,现在常用的服务器模型有循环服务器模式和并发服务器模式。
循环服务器:在同一时刻,服务器只能响应一个客户端请求;
并发服务器:在同一时刻,服务器可以响应多个客户端请求;
1、UDP循环服务器
客户端请求、处理、返回处理结果;由于UDP是面向非连接的,所以没有个一个客户端可以一直占据着服务器,即服务器一般可以满足每个客户端的请求。
2、TCP循环服务器
客户端请求、处理、完成所有请求后断开连接;由于TCP是面向连接的,所以服务器需要处理完一个客户端的所有请求并断开连接后才可以继续处理下一个客户端的请求。
3、TCP并发服务器
服务器对于客户端的每一个请求不直接处理,而是创建一个子进程处理;解决了TCP循环服务器独占问题,但是创建的子进程需要消耗大量的系统资源。
4、UDP并发服务器
与TCP并发服务器类似,一般不常用。
5、并发服务器:多路I/O复用
解决了TCP循环服务器独占问题,又避免了TCP并发服务器消耗大量系统资源的问题
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
// nfds:最大文件描述符加1;
// readfds/writefds/exceptfds:读/写/异常文件描述符集;
// timeout:超时时间;
timeout为NULL,阻塞,直到监视的文件描述符有变化,返回正值;
timeout为0,纯粹的非阻塞,立即返回,如果监视的文件描述符有变化返回正值,否则返回0;
timeout>0,在timeout时间内为阻塞状态,在超时时间内如果监视的文件描述符有变化则返回正值,否则与timeout为0一样;
struct timeval {
long tv_sec;
long tv_usec;
}

// fd_set/文件描述符集使用整数数组的位域表示,即数组元素的每一位对应一个文件描述符;
// 一般操作系统中定义FD_SETSIZE声明fd_set文件描述符的最大数目,如#define FD_SETSIZE 1024;即32个元素的整数数组(元素:4Bytes 32bit)
void FD_CLR(int fd, fd_set *set); // 设置整数数组中对应文件描述符的位为0;
void FD_SET(int fd, fd_set * set); // 设置整数数组中对应文件描述符的位为1;
void FD_ZERO(fd_set *set); // 设置整数数组中所有的位为0;
int FD_ISSET(int fd, fd_set *set); // 判断整数数组中对应文件描述符的位是否为1;

参考的文章:
一切皆socket
linux raw socket
linux 网络编程入门
以上为个人笔记,如有错误还望指出,唯有时间了解爱

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值