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可能会有三种行为:

  1. l_onoff:关闭时(值为0),close默认行为,发送缓冲区所有数据后关闭连接
  2. l_onoff:打开时(值大于0),若l_linger为0,close系统调用立即返回,缓冲区数据被丢弃,给对端发送RST报文
  3. l_onoff:打开时(值大于0),若l_linger大于0:
    1. 阻塞型socket,close等待l_linger的时间,直到发送完缓冲区数据并收到对端的ACK,如果这段时间没有发送完缓冲区数据并收到确认,close将返回-1并设置errno为EWOULDBLOCK。
    2. 非阻塞型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的更多相关文章

  1. JAVA Socket API与LINUX Socket API探究

    代码 这是一个带有UI界面的JAVA网络聊天程序,使用Socket连接完成通信. JAVA服务端程序 import java.io.IOException; import java.io.InputS ...

  2. Java实现网络聊天中使用的socket API与Linux socket API之间的关系

    尝试着用Java编写一个网络聊天程序,发现总不如网上写的好,所以就直接引用了网上大神的优秀代码.代码如下: package project1; import java.awt.*; import ja ...

  3. linux socket高性能服务器处理框架

    这个博客很多东西 http://blog.csdn.net/luozhonghua2014/article/details/37041765   思考一种高性能的服务器处理框架 1.首先需要一个内存池 ...

  4. Linux socket编程 DNS查询IP地址

    本来是一次计算机网络的实验,但是还没有完全写好,DNS的响应请求报文的冗余信息太多了,不只有IP地址.所以这次的实验主要就是解析DNS报文.同时也需要正确的填充请求报文.如果代码有什么bug,欢迎指正 ...

  5. Linux Socket 编程简介

    在 TCP/IP 协议中,"IP地址 + TCP或UDP端口号" 可以唯一标识网络通讯中的一个进程,"IP地址+端口号" 就称为 socket.本文以一个简单的 ...

  6. OpenFastPath(2):原生态Linux Socket应用如何移植到OpenFastPath上?

    版本信息: ODP(Open Data Plane): 1.19.0.2 OFP(Open Fast Path): 3.0.0 1.存在的问题 OpenFastPath作为一个开源的用户态TCP/IP ...

  7. linux socket can测试

    1. Overview / What is Socket CAN -------------------------------- The socketcan package is an implem ...

  8. socket编程 ------ BSD socket API

    伯克利套接字(Berkeley sockets),也称为BSD Socket.伯克利套接字的应用编程接口(API)是采用C语言的进程间通信的库,经常用在计算机网络间的通信. BSD Socket的应用 ...

  9. TCP协议和socket API 学习笔记

    本文转载至 http://blog.chinaunix.net/uid-16979052-id-3350958.html 分类:  原文地址:TCP协议和socket API 学习笔记 作者:gilb ...

  10. Linux socket 编程中存在的五个隐患

    前言:         Socket API 是网络应用程序开发中实际应用的标准 API.尽管该 API 简单,但是   开发新手可能会经历一些常见的问题.本文识别一些最常见的隐患并向您显示如何避免它 ...

随机推荐

  1. 简单的python3脚本:从日志中提取信息

    命名:log_extractor.py or download_stats_extractor.py # coding:utf-8 #!/usr/bin/python3 def filter_line ...

  2. python连接数据库及查询包含中文错误解决方法

    使用MySQLdb库来连接数据库 import MySQLdb conn = MySQLdb.connect(host='127.0.0.1', user='root', passwd='', por ...

  3. C++子类的构造函数

    子类的构造函数 子类可以有自己的构造函数 子类没有构造函数,默认系统会调用父类的构造函数 子类有自己的构造函数,系统会先运行父类的构造函数,随后运行子类的构造函数,对子类对象进行覆盖和拓展 即不论子类 ...

  4. FreeSWITCH添加自定义endpoint之媒体交互

    操作系统 :CentOS 7.6_x64 FreeSWITCH版本 :1.10.9   之前写过FreeSWITCH添加自定义endpoint的文章: https://www.cnblogs.com/ ...

  5. Anaconda+PyCharm+Pytorch/tensorflow环境配置个人总结

    Anaconda是一个非常方便的python版本管理工具,可以很方便地切换不同版本的Python进行测试.同时不同版本之间也不存在相互的干扰. PyCharm是一款常见的Python IDE,pyto ...

  6. 浏览器Xbox 云游戏教程

    我这里使用的是韩国的地方因为延迟和网速会比较快 Xbox 云游戏韩国网站 Xbox.com에서 Xbox Cloud Gaming(베타) 首先插件商店下载一个油猴插件 在系统语言和时区改为韩国 在  ...

  7. 给你安利一款带有AI功能的数据库管理工具

    写在前面 说到数据库管理工具,大家应该不陌生了 小伙伴们应该都用过Navicat.DBever.DataGrip.SQLyog.plsqldeveloper等数据库管理工具 这些工具呢都各自有优缺点. ...

  8. [语音识别] 基于Python构建简易的音频录制与语音识别应用

    语音识别技术的快速发展为实现更多智能化应用提供了无限可能.本文旨在介绍一个基于Python实现的简易音频录制与语音识别应用.文章简要介绍相关技术的应用,重点放在音频录制方面,而语音识别则关注于调用相关 ...

  9. SNAT与DNAT原理及应用

    SNAT与DNAT原理及应用 当内部地址要访问公网上的服务时(如httpd访问),内部地址会主动发起连接,由路由器或者防火墙上的网关对内部地址做个地址转换,将内部地址的私有IP转换为公网的公有IP,网 ...

  10. fastapi启动后访问docs不显示页面的问题

    笔者之前正常使用fastapi的docs接口进行各种接口调试,使用很正常,之前安装也都是正常安装流程,没有做任何修改,可以突然有一天不知道为啥,docs接口打开是空白的,接口也没有报错,就是空白,摸索 ...