Linux socket API
socket是进程通信机制的一种,与PIPE、FIFO不同的是,socket即可以在同一台主机通信(unix domain),也可以通过网络在不同主机上的进程间通信(如:ipv4、ipv6),例如因特网,应用层通过调用socket API来与内核TCP/IP协议栈的通信,通过网络字节实现不用主机之间的数据传输。
前置条件
字节序
对于多字节的数据,不同处理器存储字节的顺序称为字节序,主要有大端序(big-endian)和小端序(little-endian),字节序的收发不统一就会导致值被解析错误。
大端序
高位字节存低位内存
大端序是最高位字节存储在最低位内存地址处。例如一段数据0x0A0B0C0D,0x0A是最高位字节,0x0D是最地位字节,内存地址最低位a、最高位a+3,在大端序中存储方式如下

- 8bit存储方式:内存地址从低到高0x0A -> 0x0B -> 0x0C -> 0x0D
- 16bit存储方式:内存地址从低到高0x0A0B -> 0x0C0D
小端序
低位字节存低位内存
小端序是最低位字节存储在最低位内存地址处。例如一段数据0x0A0B0C0D,0x0A是最高位字节,0x0D是最地位字节,内存地址最低位a、最高位a+3,在小端序存储方式如下

- 8bit存储方式:内存地址从低到高0x0D->0X0C->0X0B->0X0A
- 16bit存储方式:内存地址从低到高0X0C0D->0X0A0B
主机通常使用小端序,因为计算机先处理小端序的字节效率更高。通过上面的结构不难看出,大端序更易读,所以网络和存储等采用了大端序,那么网络通信的时候就需要将网络字节的大端序转换为主机字节的小端序。好在这些都有系统调用可以保证~
判断主机的字节序:
#include <iostream>
using namespace std;
void byteorder() {
union {
short value;
char union_bytes[sizeof(short)];
} test;
test.value = 0x0102;
if ((test.union_bytes[0] == 0x01) && (test.union_bytes[1] == 0x02)) {
cout << "big endian" << endl; // [0x01, 0x02]
} else if ((test.union_bytes[0] == 0x02) && (test.union_bytes[1] == 0x01)) {
cout << "little endian" << endl; // [0x02, 0x01]
} else {
cout << "unknow~" << endl;
}
}
int main() { byteorder(); }
字节序转换
#include<netinet/in.h>
// long型主机字节序转换为long型网络字节序, host to network
unsigned long int htonl(unsigned long int hostlong);
// short型
unsigned short int htons(unsigned short int hostshort);
// long型网络字节序转换为long型主机字节序, network to host
unsigned long int ntohl(unsigned long int netlong);
// short型
unsigned short int ntohs(unsigned short int netshort);
比方转换主机的端口
int main(int argc, char *argv[]){
int port = atoi(argv[1]); // 主机序
server_address.sin_port = htons(port); // 网络序
}
地址
通用地址
地址我们标识通信的端点,通用的地址格式为
#include<bits/socket.h>
struct sockaddr
{
sa_family_t sa_family; // 协议类型,例如 ipv4 AF_INET、unix AF_UNIX
char sa_data[14]; // unix域存放文件路径,ip域存放ip地址和端口号
}
sa_data只能容纳14字节地址数据,如果是unix域路径长度可以达到108字节放不下,所以linux定义了新的地址
#include<bits/socket.h>
struct sockaddr_storage
{
sa_family_t sa_family;
unsigned long int__ss_align; // 作用是内存对齐
char__ss_padding[128-sizeof(__ss_align)];
}
专有地址
专有地址在bind、accept、connect等需要用到的函数中需要强制转换为通用地址,例如:(struct sockaddr *)&server_address
顾名思义专门为ipv4、unix、ipv6设计的不同socket地址结构,以ipv4为例
struct sockaddr_in
{
sa_family_t sin_family; // AF_INET
u_int16_t sin_port; // 网络字节序的端口号
struct in_addr sin_addr; // IP地址
};
struct in_addr
{
u_int32_t s_addr; // 网络字节序的IP地址
};
具体这样用:
int main(int argc, char *argv[]) {
const char *ip = argv[1]; // 主机序ip地址
int port = atoi(argv[2]); // 主机序端口
struct sockaddr_in address; // ipv4专有地址
// 设置专有地址的成员
address.sin_family = AF_INET;
address.sin_port = htons(port);
// 将点分10进制的ip字符串转换为网络字节序整形表示的ip地址,存入sin_addr
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建socket
// 绑定端口,要强制转换为通用地址 (struct sockaddr *)&address
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
}
创建连接
创建socket
Linux一切皆文件,所以socket创建好之后就是一个文件描述符,对该fd读写关闭、属性控制。
以ipv4为例
#include <sys/socket.h>
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
- 第一个参数domain指定协议族,AF_INET、AF_UNIX、AF_INET6
- 第二个参数type指定socket类型,TCP\UDP分别使用流式SOCK_STREAM和数据报式SOCK_DGRAM
- 第三个参数protocal指定协议,有IPPROTO_TCP、IPPROTO_ICMP、IPPROTO_UDP等。通常使用默认的0。例如domain为AF_INET,type为SOCK_STREAM,那么就意味着ipv4 TCP类型的socket,protocal设置为0即可。
标识socket:bind
标识该socket,对于ipv4用ip地址和端口作为端点的表示
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
成功返回0,失败返回-1并设置errno,例如errno
- EACCES:没有权限绑定该端口
- EADDRINUSE:绑定一个没有释放的端口和地址,通常被处于TIME_WAIT的连接使用,需要使用
SO_REUSEADDR来复用处于TIME_WAIT连接的端口和地址
监听socket:listen
开始监听,并指定连接数
#include<sys/socket.h>
int listen(int sockfd,int backlog);
ret = listen(sock, 5);
- backlog参数表示处于ESTABLISHED状态的连接数(我的ubuntu20.4测试为backlog+1),超过该值客户端收到ECONNREFUSED或者客户端TIMEOUT
接受连接:accept
从listen队列中拿连接过来,不管该连接是ESTABLISED还是CLOSE_WAIT的状态。
int connfd = accept(sockfd, (struct sockaddr *)&client, &client_addrlength);
发起连接:connect
connect(sockfd, (struct sockaddr *)&server_address, sizeof(server_address))
成功返回0,失败返回-1并设置errno
- ECONNREFUSED:目标端口不存在或连接被拒绝
- ETIMEOUT:连接超时
关闭连接
close
关闭socket fd,默认情况下:如果是多进程,fork后会将fd引用计数加1,如果要关闭该socket,父子进程都需要close,而且是同时关闭读和写。可以通过setsockopt的SO_LINGER控制close的行为
#include<sys/socket.h>
struct linger
{
int l_onoff; // 关闭控制
int l_linger; // 控制时间
}
close可能会有三种行为:
- l_onoff:关闭时(值为0),close默认行为,发送缓冲区所有数据后关闭连接
- l_onoff:打开时(值大于0),若l_linger为0,close系统调用立即返回,缓冲区数据被丢弃,给对端发送RST报文
- l_onoff:打开时(值大于0),若l_linger大于0:
- 阻塞型socket,close等待l_linger的时间,直到发送完缓冲区数据并收到对端的ACK,如果这段时间没有发送完缓冲区数据并收到确认,close将返回-1并设置errno为EWOULDBLOCK。
- 非阻塞型socket,立即返回,根据返回值和errno来判断残留数据是否发送完毕
shutdown
#include<sys/socket.h>
int shutdown(int sockfd,int howto);
不引用计数直接关闭,howto参数:
- SHUT_RD:程序不能再对socketfd做读操作,接收缓冲区数据被丢掉
- SHUT_WR:关闭socketfd写,缓冲区数据会在关闭前发送出去,写操作不可执行(半关闭状态)
- SHUT_RDWR:同时关闭
数据读写
除了默认对文件描述符的read、write操作之外,socket提供了专门的读写数据函数
TCP读写(recv & send)
#include<sys/socket.h>
// recv成功时返回读取到的长度,实际长度可能小于len
// 发生错误返回-1设置errno,返回0表示连接关闭
ssize_t recv(int sockfd, void*buf, size_t len, int flags);
// 成功时返回写入的数据的长度,失败返回-1这是errno
ssize_t send(int sockfd, const void*buf, size_t len, int flags);
flags提供了一些选项设置:
- MSG_OOB(recv&send):发送或接收紧急数据,也叫带外数据,在传输层的七七八八中首部信息中有说,在URG标志位1时该字段有效,seq + Urgen Pointer - 1的这一个字节是紧急数据(紧急数据只有一个字节),例如:
char buffer[1024];
memset(buffer, '\0', 1024);
// 发送端发送带外数据hello
const char *oob_data = "hello";
send(sockfd, oob_data, strlen(oob_data), MSG_OOB);
ret = recv(connfd, buffer, BUFESIZE - 1, 0);
// 接收到hell
ret = recv(connfd, buffer, BUFESIZE - 1, MSG_OOB); // 接收端接收带外数据
// 接收到o
hell为正常数据,o为带外数据,只有最后一个字节会被认为是带外数据,前面的是正常数据。正常数据的接收会被带外数据截断。
int sockatmark(int sockfd);可以判断下一个数据是不是带外数据,1为是,此时可以利用MSG_OOB标志的recv调用来接收带外数据。- 通过SIGUSR信号触发对带外数据的处理
- MSG_DONTWAIT(recv&send):对socket的此次send或recv是非阻塞操作(相当于使用O_NONBLOCK)
- MSG_WAITALL(recv):一直读取到请求的数据全部返回后recv函数返回
UDP读写(recvfrom & sendto)
通常这两个函数用于无连接的套接字,如果用于有连接的读写可以把后两位置为NULL
#include <sys/socket.h>
// 可以接收UDP,也可以接收TCP(后两个参数置位NULL,因为TCP是面向连接的)
ssize_t recvfrom(int sockfd,void* buf,size_t len,int flags,
struct sockaddr* src_addr,socklen_t* addrlen);
// 可以接收UDP,也可以接收TCP(后两个参数置位NULL,因为TCP是面向连接的)
ssize_t sendto(int sockfd,const void* buf,size_t len,int flags,
const struct sockaddr* dest_addr,socklen_t addrlen);
更高级的读写(recvmsg & sendmsg)
使用sendmsg可以将多个缓冲区的数据合并发送
使用recvmsg可以将接收的数据送入多个缓冲区,或者接收辅助数据
#include<sys/socket.h>
ssize_t recvmsg(int sockfd,struct msghdr* msg,int flags);
ssize_t sendmsg(int sockfd,struct msghdr* msg,int flags);
msghdr结构
struct msghdr
{
void* msg_name; // socket地址,如果是流数据,设置为NULL
socklen_t msg_namelen; // 地址长度
struct iovec* msg_iov; // I/O缓存区数组,分散的缓冲区
int msg_iovlen; // I/O缓存区数组元素数量
void* msg_control; // 辅助数据起始位置
socklen_t msg_controllen; // 辅助数据字节数
int msg_flags; // 等于recvmsg和sendmsg的flags参数,在调用过程中更新
};
辅助函数
获取地址
#include<sys/socket.h>
// 获取socketfd本端的地址信息,存到address,如果address长度大于address_len,将被截断
int getsockname(int sockfd,struct sockaddr*address,socklen_t*address_len);
// 获取socketfd远端的地址信息
int getpeername(int sockfd,struct sockaddr*address,socklen_t*address_len);
成功返回0,失败返回-1设置errno
socketfd属性设置,option
#include<sys/socket.h>
int getsockopt(int sockfd,int level,int option_name,
void*option_value,socklen_t*restrict option_len);
int setsockopt(int sockfd,int level,int option_name,
const void*option_value,socklen_t option_len);
成功返回0,失败返回-1设置errno,记录一下option_name,后面用到结合具体实例分析

gethostbyname & gethostbyaddr
根据主机名称获取主机的完整信息、根据地址获取主机的完整信息,信息返回结构如下:
#include<netdb.h>
struct hostent
{
char* h_name; /*主机名*/
char** h_aliases; /*主机别名列表,可能有多个*/
int h_addrtype; /*地址类型(地址族)*/
int h_length; /*地址长度*/
char** h_addr_list /*按网络字节序列出的主机IP地址列表*/
};
getservbyname & getservbyport
根据服务名称或端口号获取服务信息,从/etc/services获取信息,该文件中存放的是知名端口号和协议等信息。返回结构体如下:
#include<netdb.h>
struct servent
{
char* s_name; /*服务名称*/
char** s_aliases; /*服务的别名列表,可能有多个*/
int s_port; /*端口号*/
char* s_proto; /*服务类型,通常是tcp或者udp*/
};
getaddrinfo
可以认为是调用了gethostbyname和getservbyname
#include<netdb.h>
// hostname:可以是主机名或IP地址字符串
// service:可以接收服务名,也可以接收十进制端口号
// result指向返回结果的链表,结构为addrinfo
int getaddrinfo(const char* hostname,const char* service,const
struct addrinfo* hints,struct addrinfo** result);
addrinfo结构体:
struct addrinfo
{
int ai_flags; /*大部分设置hints参数*/
int ai_family; /*地址族*/
int ai_socktype; /*服务类型,SOCK_STREAM或SOCK_DGRAM*/
int ai_protocol; /*通常设置为0*/
socklen_t ai_addrlen; /*socket地址ai_addr的长度*/
char* ai_canonname; /*主机的别名*/
struct sockaddr* ai_addr; /*指向socket地址*/
struct addrinfo* ai_next; /*指向下一个sockinfo结构的对象*/
};
getaddrinfo结束后,释放result分配的堆内存
void freeaddrinfo(struct addrinfo* res);
getnameinfo
可以认为是调用了gethostbyaddr和getservbyport
#include<netdb.h>
// 返回的主机名存储在host,服务名存储在serv
int getnameinfo(const struct sockaddr *sockaddr,socklen_t addrlen,
char* host,socklen_t hostlen,char *serv,socklen_t servlen,int flags);
gai_strerror
转换getnameinfo和getaddrinfo返回的错误码为可读的字符串
#include<netdb.h>
const char* gai_strerror(int error);
getaddrinfo和getnameinfo返回的错误码如下:

简单示例
testserver.cc,testserver 0.0.0.0 8889
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <cstdio>
#include <iostream>
using namespace std;
int main(int argc, char *argv[]) {
if (argc <= 2) {
cout << "usage:" << argv[0] << " ip_address port_number" << endl;
return 0;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address, client_addr;
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int ret = bind(sockfd, (struct sockaddr *)&address, sizeof(address));
assert(ret != -1);
ret = listen(sockfd, 2);
assert(ret != -1);
socklen_t client_addr_length = sizeof(client_addr);
int conn =
accept(sockfd, (struct sockaddr *)&client_addr, &client_addr_length);
if (conn < 0)
cout << "connect error: " << errno << endl;
else {
string hello = "hello client";
send(conn, hello.data(), sizeof(hello), 0);
close(conn);
}
close(sockfd);
return 0;
}
testclient.cc,/etc/hosts加入server的地址和主机名,testclient myserver
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cassert>
#include <iostream>
using namespace std;
int main(int argc, char* argv[]) {
if (argc != 2) {
cout << "usage: " << argv[0] << " hostname" << endl;
return 0;
}
char* hostname = argv[1];
// 获取主机信息
struct hostent* hostinfo = gethostbyname(hostname);
assert(hostinfo);
/*
获取server返回信息,自定义一个服务,
编辑/etc/services, my 8889/tcp
*/
struct servent* servinfo = getservbyname("my", "tcp");
assert(servinfo);
cout << "myserver port is " << ntohs(servinfo->s_port) << endl;
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = servinfo->s_port;
address.sin_addr = *(struct in_addr*)*hostinfo->h_addr_list;
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
int result = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
assert(result != -1);
char buffer[128];
result = recv(sockfd, buffer, sizeof(buffer), 0);
cout << "resceived: " << result << endl;
assert(result > 0);
buffer[result] = '\0';
cout << "server's message: " << buffer << endl;
close(sockfd);
return 0;
}
学习自:
《Linux高性能服务器编程》
《UNIX环境高级编程》
《UNIX系统编程》
Linux socket API的更多相关文章
- JAVA Socket API与LINUX Socket API探究
代码 这是一个带有UI界面的JAVA网络聊天程序,使用Socket连接完成通信. JAVA服务端程序 import java.io.IOException; import java.io.InputS ...
- Java实现网络聊天中使用的socket API与Linux socket API之间的关系
尝试着用Java编写一个网络聊天程序,发现总不如网上写的好,所以就直接引用了网上大神的优秀代码.代码如下: package project1; import java.awt.*; import ja ...
- linux socket高性能服务器处理框架
这个博客很多东西 http://blog.csdn.net/luozhonghua2014/article/details/37041765 思考一种高性能的服务器处理框架 1.首先需要一个内存池 ...
- Linux socket编程 DNS查询IP地址
本来是一次计算机网络的实验,但是还没有完全写好,DNS的响应请求报文的冗余信息太多了,不只有IP地址.所以这次的实验主要就是解析DNS报文.同时也需要正确的填充请求报文.如果代码有什么bug,欢迎指正 ...
- Linux Socket 编程简介
在 TCP/IP 协议中,"IP地址 + TCP或UDP端口号" 可以唯一标识网络通讯中的一个进程,"IP地址+端口号" 就称为 socket.本文以一个简单的 ...
- OpenFastPath(2):原生态Linux Socket应用如何移植到OpenFastPath上?
版本信息: ODP(Open Data Plane): 1.19.0.2 OFP(Open Fast Path): 3.0.0 1.存在的问题 OpenFastPath作为一个开源的用户态TCP/IP ...
- linux socket can测试
1. Overview / What is Socket CAN -------------------------------- The socketcan package is an implem ...
- socket编程 ------ BSD socket API
伯克利套接字(Berkeley sockets),也称为BSD Socket.伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信. BSD Socket的应用 ...
- TCP协议和socket API 学习笔记
本文转载至 http://blog.chinaunix.net/uid-16979052-id-3350958.html 分类: 原文地址:TCP协议和socket API 学习笔记 作者:gilb ...
- Linux socket 编程中存在的五个隐患
前言: Socket API 是网络应用程序开发中实际应用的标准 API.尽管该 API 简单,但是 开发新手可能会经历一些常见的问题.本文识别一些最常见的隐患并向您显示如何避免它 ...
随机推荐
- Nginx:超时 keeplive_timeout 配置
参考:Nginx的超时keeplive_timeout配置详解 HTTP 是一种无状态协议,客户端向服务器发送一个 TCP 请求,服务端响应完毕后断开连接. 如果客户端向服务器发送多个请求,每个请求都 ...
- 二进制安装K8S
参考链接:https://zhuanlan.zhihu.com/p/408967897 准备工作 3台Centos7.9虚拟机 虚拟机配置:2C4G,能连接外网 虚机规划 ip 用途 192.168. ...
- chrome pre 自动换行
问题引出 当我想要使用chrome的打印功能生成一份关于md的pdf版本的时候发现有的代码块没有自动换行,生成的PDF没有自动换行,导致部分信息无法阅读 处理方式 把有自动换行的部分处理一下,在md文 ...
- python-多继承构造函数声明问题
背景 有场景分别定义两组逻辑,随后有统一入口做基类属性的整合 其中两组逻辑的积累构造函数定义入参不同 设计类继承图如: 实际的使用方式抽象为[使用] 小节 实际开发过程中遇到问题 先说结论 pytho ...
- 2、搭建MyBatis
2.1.开发环境 IDE:idea 2019.2 构建工具:maven 3.8.4 MySQL版本:MySQL 5.7 MyBatis版本:MyBatis 3.5.7 MySQL不同版本的注意事项 ( ...
- 三维模型OBJ格式轻量化压缩变形现象分析
三维模型OBJ格式轻量化压缩变形现象分析 三维模型的OBJ格式轻量化压缩是一种常见的处理方法,它可以减小模型文件的体积,提高加载和渲染效率.然而,在进行轻量化压缩过程中,有时会出现模型变形的现象,即压 ...
- RabbitMQ 如何实现延迟队列?
延迟队列是指当消息被发送以后,并不是立即执行,而是等待特定的时间后,消费者才会执行该消息. 延迟队列的使用场景有以下几种: 未按时支付的订单,30 分钟过期之后取消订单. 给活跃度比较低的用户间隔 N ...
- Java实现常见查找算法
Java实现常见查找算法 查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找. 线性查找 线性查找(Linear Search)是一种简单的查 ...
- 4-MySQL数据库的常用操作
在MySQL数据库中,增删改查操作是指对数据进行添加.删除.查询和修改的操作.这些操作在数据库管理和维护中非常重要,可以帮助数据库管理员和开发人员有效地管理数据和实现各种复杂的数据处理需求. 添加数据 ...
- Three.js中实现一个OBBHelper
1. 引言 Three.js中,Box3对象指的是AABB式的包围盒,这种包围盒会随物体的旋转而变换大小,精度较差 Three.js中还有OBB对象,这是一种能表现物体主要特征的.不随物体的旋转而变换 ...