网络编程套接字(Socket)

版权声明:本文为博主原创文章,欢迎转载,转载请声明出处! https://blog.csdn.net/hansionz/article/details/85226345

网络预备知识学习:https://blog.csdn.net/hansionz/article/details/85224786

一.IP地址和端口号

1.IP地址

  • IPv4版本的IP地址为4字节,也就是32位
  • 网络层的数据报中封装两个IP地址,一个源IP地址(数据报源主机的IP),一个目的IP地址(数据报目的主机的IP)
  • 源IP地址目的IP地址相当于取经的例子,在预备知识中可以看到
  • 一个数据报的头部不应该只存在源IP地址和目的IP地址,还应该存在一个协议字段告诉应该交给上层的哪一个协议

2.端口号

2.1 什么是端口号

  • 端口号是传输层的概念
  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程,用来告诉OS,将数据交给哪一个进程
  • IP地址+端口号可以唯一的表示网络中一个主机的进程
  • 一个端口号只能被一个进程占用

2.2 端口号和进程ID

一个进程PID也可以唯一的标识一个进程,那为什么还需要使用端口号来标识一个主机中的进程呢?

  • 当一个进程退出时,在次启动进程时,它的PID已经变化。所以要将进程和端口号绑定来标识这个进程
  • 一个进程可以绑定多个端口号,但是一个端口号只能被一个进程绑定

2.3 源端口号和目的端口号

传输层协议TCP/UDP数据段中,也存在两个字段,一个是源端口号,一个是目的端口号,它用来表示这个数据是哪一个进程发的,要发给哪一个进程,也就是谁发的要发给谁

二.初识TCP/UDP协议和网络字节序

1.TCP(传输控制协议)

  • 传输层协议
  • 面向连接,两个主机只有建立连接之后才可以通信
  • 可靠传输,建立连接之后,占用端到端的通信资源
  • 面向字节流,发送的字节流数据之间没有明确的间隔

2.UDP(用户数据报协议)

  • 传输层协议
  • 面向无连接,尽最大努力交付(例如:发短信,不管能不能收到,都可以发送)
  • 不可靠的传输
  • 面向数据报,发送的一个数据是一个整体,它们之间有明确的间隔

3.网络字节序

内存中大于1个字节的数据相对于内存地址存在大小端之分,磁盘文件中多字节数据相对于偏移地址也存在大小端之分,网络中的数据流也存在大小端之分。

什么是大小端之分?

单独总结我的另一篇博客:https://blog.csdn.net/hansionz/article/details/80871921

网络字节流的大小端:

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

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数对网络字节序和主机字节序的转换:

#include <arpa/inet.h>
//h--host主机,n--net网络,l--long长整型32位,s--short短整型16位
uint32_t htonl(uint32_t hostlong);//主机-->网络(32位)
uint16_t htons(uint16_t hostshort);//主机-->网络(16位)
uint32_t ntohl(uint32_t netlong);//网络-->主机(32位)
uint16_t ntohs(uint16_t netshort);//网络-->主机(16位)

三.Socket编程

1.Udp socket常见接口

  • socket(创建套接字)
#include <sys/types.h>         
#include <sys/socket.h>
//创建socket文件描述符,相当于创建一个设备文件,来实现传输层之间的通信
//(客户端+服务器)
int socket(int domain, int type, int protocol);
参数:
	domain:代表通信协议族
			AF_INET---IPv4的协议
			AF_INET6--IPv6的协议
	type:代表创建什么类型的套接字
			SOCK_STREAM:流式套接字(TCP)
			SOCK_DGRAM:数据报套接字(UDP)
	protocol:具体的协议
			0代表默认协议
返回值:
	返回该套接字的文件描述符(句柄)
  • bind(绑定端口号)
#include <sys/types.h>       
#include <sys/socket.h>
//用来绑定端口号(服务器)
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
参数:
	sockfd:socket接口的返回值,既创建套接字的描述符
	addr:类型为struct sockaddr,该结构体下边有详细说明,参数代表地址
	addrlen:地址信息长度

sockaddr结构体:

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6UNIX Domain Socket。 但是各种网络协议的地址格式并不相同:

struct sockaddr {
     sa_family_t sa_family;
     char  sa_data[14]; /*地址信息*/
}

sockaddr_in结构体:

虽然socket API的类型是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in。 这个结构里主要有三部分信息地址类型、端口号、IP地址

struct sockaddr_in {
    short            sin_family;       // 2 bytes e.g. AF_INET, AF_INET6
    unsigned short   sin_port;         // 2 bytes e.g. htons(3490)
    struct in_addr   sin_addr;         // 4 bytes see struct in_addr, below
    char             sin_zero[8];      // 8 bytes zero this if you want to
};
//in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数
struct in_addr {
    unsigned long s_addr;          // 4 bytes load with inet_pton()
};

可以将sockaddr类似于void*,将sockaddr_in类似于int*

在这里插入图片描述

  • IPv4和IPv6的地址格式定义在netinet/in.h
  • IPv4地址用sockaddr_in结构体表示,包括16位地址类型,16 位端口号32位IP地址
  • IPv4、IPv6地址类型分别定义为常数AF_INETAF_INET6。只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API可以都用struct sockaddr *类型表示,在使用的时候需要强制转化成sockaddr_in。这样的好处是程序的通用性,可以接收IPv4、IPv6、UNIX Domain Socket各种类型的sockaddr结构体指针做为参数

2.地址转化函数

sockaddr_in中的成员struct in_addr sin_addr表示32位IP地址。但是实际中,我们通常使用点分十进制的字符串来标识IP地址。所以,我们应该使用一些地址转化函数来对IP地址进行字符串到in_addr相互转化。

  • 字符串转in_addr的函数
in_addr_t inet_addr(const char *cp);
int inet_aton(const char *cp, struct in_addr *inp);
int inet_pton(int af, const char *src, void *dst);
  • in_addr转字符串的函数
char *inet_ntoa(struct in_addr in);
const char *inet_ntop(int af, const void *src,char *dst, socklen_t size);

注:其中inet_ptoninet_ntop不仅可以转换IPv4in_addr,还可以转换IPv6in6_addr

inet_ntoa函数:

inet_ntoa函数返回了一个char*, 这个函数自己在内部为我们申请了一块内存来保存ip的结果。man手册上说,inet_ntoa函数将返回结果放到了静态存储区, 不需要我们手动释放。但是如果放在静态区的话,这个函数多次被调用就会覆盖掉静态区的内容。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <iostream>
using namespace std;
int main()
{
  struct sockaddr_in addr1;
  struct sockaddr_in addr2;

  addr1.sin_addr.s_addr = 0x0;
  addr2.sin_addr.s_addr = 0xffffffff;
  char* p1 = inet_ntoa(addr1.sin_addr);
  char* p2 = inet_ntoa(addr2.sin_addr);

  cout << p1 << endl << p2 << endl;
  return 0;
}

下边的运行结果可以看出IP地址确实被覆盖了。
在这里插入图片描述

就上述情况而言,如果是多个线程去调用inet_ntoa函数,会不会出现问题?

  • Man手册明确提出inet_ntoa不是线程安全的函数
  • 但是我自己在centos7上测试,并没有出现问题, 可能内部的实现加了互斥锁
  • 多线程环境下,推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果,可以规避线程安全问题

测试代码:

#include <iostream>
#include <unistd.h> 
#include <sys/socket.h>
#include <netinet/in.h> 
#include <arpa/inet.h>
#include <pthread.h>

using namespace std;

void* Routine1(void* arg)
{
  struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
  while(1)
  {
    sleep(1);
    char* p = inet_ntoa(ptr->sin_addr);
    cout << "pthread 1:" << p << endl;
  }
  return NULL;
}
void* Routine2(void* arg)
{

  struct sockaddr_in* ptr = (struct sockaddr_in*)arg;
  while(1)
  {
    sleep(1);
    char* p = inet_ntoa(ptr->sin_addr);
    cout << "pthread 2:" << p << endl;
  }
  return NULL;
}
int main()
{
  struct sockaddr_in addr1;
  struct sockaddr_in addr2;
  addr1.sin_addr.s_addr = 0x0;
  addr2.sin_addr.s_addr = 0xffffffff;

  pthread_t t1;
  pthread_t t2;
  pthread_create(&t1, NULL, Routine1, (void*)&addr1);
  pthread_create(&t2, NULL, Routine2, (void*)&addr2);

  pthread_join(t1, NULL);
  pthread_join(t2, NULL);
  return 0;
}

测试结果:
在这里插入图片描述

3. 常见Tcp sockect接口

  • sockect(创建套接字)
    在这里插入图片描述

参数及返回值说明:

  • 该函数打开一个网络通讯端口,成功像open一样返回一个文件描述符,调用出错返回1
  • 应用程序可以像读写文件一样用read/write在网络上收发数据
  • IPv4协议, domain参数指定为AF_INET
  • TCP协议,type参数指定为SOCK_STREAM ,表示面向字节流的传输协议
  • protocol参数默认为0即可

  • Bind(绑定)

在这里插入图片描述
参数及返回值说明:

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

  • listen(监听)
    在这里插入图片描述

参数及返回值说明:

  • listen声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略,这里设置不会太大(一般是5)设置backlog是为了更加合理的利用资源,假设我们没有设置backlog,所有的连接都被占满,当来一个连接时,我们将它拒绝,而这时,有一个连接释放就会导致这个资源的不合理利用。但是太大也不行,太大会导致排在后边的连接可能要等待很长的时间。
  • listen成功返回0,失败返回-1。

  • accept(服务器接收连接请求)
    在这里插入图片描述

参数及返回值说明:

  • 三次握手完成后,服务器调用accept接受连接。如果服务器调用accept时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来

  • addr是一个输出型参数,accept返回时传出客户端的地址和端口号。如果给addr 参数传NULL,表示不关心客户端的地址

  • addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的,缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区)。

  • accept函数返回值是一个的套接字描述符。新老套接字描述符就像古代饭店拉客的情况,老的套接字描述符是为了建立连接(对应门外拉客的工作人员),而新的套接字描述符是为了后续和客户端通信(对应室内的服务人员)。

  • connect(客户端请求建立连接)
    在这里插入图片描述

参数及返回值说明:

  • 客户端需要调用connect连接服务器。connectbind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址
  • connect成功返回0,出错返回-1

猜你喜欢

转载自blog.csdn.net/hansionz/article/details/85226345