Linux网络编程套接字知识总结

IP地址与端口号

  • IP地址能够标识网络上的某一台主机。
  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。

端口号与进程ID的区别

  • 两者都可以唯一的表示一个进程。
  • 但是,一个进程的ID,当该进程结束时,此进程ID就会被释放,下次运行其他进程时就会占用该ID,所以在系统运行的整个周期内进程ID并不能唯一的表示一个进程。而端口号恰恰可以唯一的表示一个进程。
  • 一个进程可以绑定多个端口号,但是一个端口号不能被多个进程绑定。

TCP协议

  • 传输层协议
  • 有连接(三次握手,TCP每次建立连接时,客户端套接口和服务器套接口需要长期建立连接不中断,一个端口只能每次接收到来自同一个服务器的数据。)
  • 可靠传输(并不是100%就能发送成功,可靠传输是指发送成功了,就会知道自己成功了。发送失败了也能知道自己失败了)
  • 面向字节流

UDP协议

  • 传输层协议
  • 无连接(UDP每个客户端端口,并不需要跟服务器端口建立长期的连接,可以每次向一个服务器端口发起申请,然后下一次再向另外一个服务器端口申请接收数据,这样,一个UDP服务器接口可以一段时间内接受多个客户端发送的请求,一个客户端接口也可以短时间内接收多个服务端发来的数据。)
  • 不可靠传输(数据发送成功与否自己不知道)
  • 面向数据报

网络字节序

内存中的多字节数据相对于内存地址有大端和小端之分,例如在vs编译器中是小端(低地址低字节) 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址. TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端,就需要先将数据转成大端,否则就忽略,直接发送即可。

以下库函数可以做网络字节序和主机字节序的转换

#include<arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);
  • 其中,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

socket编程接口

socket常见API

// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);
// 绑定端口号 (TCP/UDP, 服务器) 
int bind(int socket, const struct sockaddr *address,socklen_t address_len);
// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);
// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);
// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockaddr结构

  • IPV4和IPV6的地址格式定义在netinet/in.h中,IPV4地址用sockaddr_in结构体表示,包括16位地址类型,16位端口号和32位IP地址。
  • IPV4,IPV6地址类型分别定义为常数AF_INET,AF_INET6,这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容。
  • socket API可以都用struct sockaddr* 类型表示,在使用的时候需要强制转化为sockaddr_in,这样的好处是程序的通用性,可以接受IPV4,IPV6类型的sockaddr结构体指针做为参数。
  • 虽然socket api的接口是sockaddr,但是我们真正在基于IPV4编程时,使用的数据结构是sockaddr_in。这个结构里主要有三部分信息:地址类型,端口号,IP地址。

地址转换函数

  • 字符串转in_addr的函数:
#include<arpa/inet.h>
int inet_aton(const char* strptr,struct in_addr *addrptr);
in_addr_t inet_addr(const char* strptr);
int inet_pton(int family,const char* strptr,void *addrptr);
  • in_addr转字符串的函数:
char* inet_ntoa(struct in_addr inaddr);
const char* inet_ntop(int family,const void *addrptr,char* strptr,size_t len);

关于inet_ntoa

  • inet_ntoa把结果放到自己内部的一个静态存储区, 这样第二次调用时的结果会覆盖掉上一次的结果.
  • inet_ntoa不是线程安全的函数。如果有多个线程调用 inet_ntoa, 会出现异常情况。
  • 但是在centos7上测试, 并没有出现问题, 可能内部的实现加了互斥锁。
  • 在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题。

TCP socket API

socket()

int socket(int domain,int type,int protocol);
  • socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符。
  • 应用程序可以像读写文件一样用read/write在网络上收发数据。
  • 如果socket()调用出错则返回-1。
  • 对于IPV4,family参数指定为AF_INET。
  • 对于TCP协议,type参数指定为SOCK_STREAM,表示面向流的传输协议。
  • protocol指定为0即可。

bind()

int bind(int sockfd,const struct sockaddr* addr, socklen_t addrlen);
  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
  • bind()成功返回0,失败返回-1。
  • bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
  • struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;

我们的程序中对myaddr参数这样初始化:

bzero(&servaddr,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
servaddr.sin_port=htons(SERV_PORT);
  1. 将整个结构体清零;
  2. 设置地址类型为AF_INET;
  3. 网络地址为INADDR_ANY, 这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP 地址, 这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP 地址;
  4. 端口号为SERV_PORT,我们定义为9999;

listen()

int listen(int sockfd,int backlog);
  • listen()声明sockfd处于监听状态,并且最多允许有backlog个客户端处于连接等待状态,如果接收到更多的连接请求就忽略。
  • listen()成功返回0,失败返回-1。

accept()

int accept(int sockfd,struct sockaddr* addr,socklen_t *addrlen);
  • 三次握手完成后, 服务器调用accept()接受连接;
  • 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
  • addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
  • 如果给addr 参数传NULL,表示不关心客户端的地址;
  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
  • accecpt的返回值,之后的recv()和send()操作,都需要对返回值进行操作。

connect

int connect(int sockfd,const struct sockaddr* addr,socklen_t addrlen);
  • 客户端需要调用connect()连接服务器;
  • connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
  • connect()成功返回0,出错返回-1;
发布了161 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_42837885/article/details/103217482