UNIX环境编程(c语言)--套接字--基本TCP套接字编程

准备知识

字节序

假如有一个16位的整数,它占了2字节,有两种存储方法
小端字节序:将低序字节存储在起始地址
大端字节序:将高序字节存储在起始地址

这两种方法都有系统在用,所以网络通信需要转化字节序

我们把某个系统上使用的字节序称为 主机字节序
把网络协议使用的字节序称为 网络字节序

网络字节序都是采用大端字节序
而主机字节序却没有标准而言

字节序转换函数

原型

uint16_t htons(uint16_t  h16bitval);  // 主机转网络,短型
uint32_t htonl(uint32_t  h32bitval); // 主机转网络,长型
uint16_t ntohs(uint16_t  n16bitval);// 网络转主机,短型
uint32_t ntohl(uint32_t  n32bitval);// 网络转主机,长型

h表示主机,n表示网络,s表示short,l 表示long

字节操纵函数

字节操纵函数有两组
一是

void bzero(void *dest, size_t nbytes);
void bcopy(const void *src, void *dest, size_t nbytes);
int bcmp(const void *ptr1, const void *ptr2, size_t nbytes);

其中bzero是将目标字节串中指定大小的字节数置为0,参数dest是指向首地址的指针,参数nbytes是需要设置的字节数。

bcopy函数用来复制内存的
参数src 为源内存块指针,dest 为目标内存块指针,n 为要复制的内存的前 n 个字节长度

bcmp的功能是比较ptr1和ptr2的前n个字节是否相等
如果ptr1=ptr2或n=0则返回零,否则返回非零值。bcmp不检查NULL。

二是

void *memset(void *dest, int c, size_t len);
void *memcpy(void *dest, const void *src,size_t nbytes);
int memcmp(const void *ptr1, const void *ptr2, size_t nbytes);

memset是将目标字节串的指定数目(len)置为值 c
memcpy,和bcopy作用和用法一致,注意参数顺序不一样
memcmp 功能和bcmp一致

地址转换函数

我们在表示ipv4的IP地址时习惯使用点分十进制来表示,用的是字符串的形式
但是在实际使用中需要使用IP地址的二进制形式

所以需要一个函数来进行互相转换

原型

int inet_aton(const char *strptr, struct in_addr *addrptr)
/*将字符串地址转化为二进制地址 成功返回1,否则返回0*/

char *inet_ntoa(struct in_addr inaddr);
/*将二进制地址转换为字符串地址, 返回一个点分十进制表示的字符串地址 */

其中的struct in_addr是地址结构内的成员,接下来会讲解
但是以上两个函数,只支持ipv4的地址

接下来两函数,支持ipv4和ipv6

int inet_pton(int family, const char *strptr, void *addrptr)
/*将字符串地址转化为二进制地址 成功返回1,输入无效返回0,错误返回-1*/

char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
/*将二进制地址转换为字符串地址, 返回一个点分十进制表示的字符串地址 */

两个函数的参数family 可以是 AF_INET(表示IPv4),AF_INET6 (表示IPV6)

第一个函数的参数strptr是需要转换的字符串的指针,addrptr用于存放结果

第二个函数的addrptr是需要转换的二进制,strptr是存放结果字符串的指针,len是str的长度,太小会报错

地址结构

很多套接字函数需要一个指向套接字地址结构的指针作为参数,每个协议族都有自己的地址结构体

但是最终都会强制转换为通用的socket地址结构来传参给函数

通用套接字 sockaddr 类型定义:

typedef unsigned short int sa_family_t;
struct sockaddr { 
 sa_family_t sa_family; /* 2 bytes address family, AF_xxx */
 char sa_data[14]; /* 14 bytes of protocol address */
}

ipv4对应的是sockaddr_in类型定义:

注意这里的sin_addr 是一个结构体,

typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in_addr {
 uint32_t s_addr; 
};
struct sockaddr_in {
 uint8_t sin_len;
 sa_family_t sin_family; /* 2 bytes address family, AF_xxx such as AF_INET */
 in_port_t sin_port; /* 2 bytes port*/
 struct in_addr sin_addr; /* 4 bytes IPv4 address*/
 /* Pad to size of `struct sockaddr'. */
unsigned char sin_zero[8]; /* 8 bytes unused padding data, always set be zero */
};

ipv6对应的sockaddr_in6类型定义:

typedef unsigned short sa_family_t;
typedef uint16_t in_port_t;
struct in6_addr
{
 union
 {
 uint8_t __u6_addr8[16];
 uint16_t __u6_addr16[8];
 uint32_t __u6_addr32[4];
 } __in6_u;
}
struct sockaddr_in6 {
 sa_family_t sin6_family; /*2B*/
 in_port_t sin6_port; /*2B*/
 uint32_t sin6_flowinfo; /*4B*/
 struct in6_addr sin6_addr; /*16B*/
 uint32_t sin6_scope_id; /*4B*/
};

Unix域对应的sockaddr_un类型定义:

#define UNIX_PATH_MAX 108
struct sockaddr_un {
 sa_family_t sun_family; 
 char sun_path[UNIX_PATH_MAX]; 
};

在使用地址结构前,我们一般先将所有字节都置0,使用czero或memset函数,然后在赋值

接下来我们将在使用时再讨论

基本TCP套接字编程

概要

基本流程
在这里插入图片描述

socket函数

socket函数的作用,就相当于我们要读文件时要先open
socket会创建一个socket描述符(和文件描述符一样),后续将使用它进行连接等操作

对于服务器来说,这个描述符是用于监听连接的,实际连接传输的描述符在accept处介绍

原型

int socket(int family, int type, int prttocol);
/* 成功返回描述符,出错返回-1

注意以下出参数并不是任意的搭配都是有效的
一般SOCK_STREAM是TCP/SCTP
SOCK_DGRAM是UDP
两个都可以和AF_INET、AF_INET6搭配

参数family,表示协议族,取值有

取值含义
AF_INETIPV4
AF_INET6IPV6
AF_LOCALUNIX域协议
AF_ROUTE路由套接字
AF_KEY秘钥套接字

参数type指明套接字类型

取值含义
SOCK_STREAM字节流套接字
SOCK_DGRAM数据套接字
SOCK_SEQPACKET有序分组套接字
SOCK_RAW原始套接字

参数protocol是某个协议类型常值,通常设为0,让其默认选择。

应用实例

进行tcp连接时

socket(AF_INET,SOCK_STREAM,0);

bind函数

将一个本地协议地址赋予一个套接字,就是绑定ip地址和端口的
通常服务器在启动的时候都会绑定一个众所周知的地址(如
ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,由系统自动分配一个端口号和自身的ip地址组合。

通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。

当然客户端也可以在调用connect()之前bind一个地址和端口,这样就能使用特定的IP和端口来连服务器了
原型

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 成功返回0,失败返回-1 */

参数sockfd:是socket描述字,bind()函数就是将给这个描述字绑定地址和端口
参数addrlen:对应的是地址的长度
参数addr:地址结构指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,但最终都会强制转换后赋值给sockaddr这种类型的指针传给内核

应用实例

struct socket_in   sockaddr_in;
int port = 12345;

memset(&sockaddr_in,0,sizeof(sockaddr_in));

sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(port);
sockaddr_in.sin_addr.s_addr = htonl(INADDR_ANY);   //INADDR_ANY 代表可以运行任何ip连接

bind(socket_fd, (struct sockaddr *)&sockaddr_in,sizeof(sockaddr_in));

listen函数

socket创建的描述符,默认是一个主动类型的(就是主动调用connect去连接别人的,是一个客户端),调用listen后转为主动的,并开始监听socket描述符,等待用户连接

原型

int listen(int sockfd, int backlog);
/* 成功返回0,失败返回-1 */

参数sockfd,是socket描述符
参数backlog 是最大连接个数

最大连接数说明

TCP建立连接是要进行三次握手,但是完成三次握手后,服务器需要维护这种状态:
  半连接状态为:服务器处于Listen状态时收到客户端SYN报文时放入半连接队列中,即SYN queue(服务器端口状态为:SYN_RCVD)。
  全连接状态为:TCP的连接状态从服务器(SYN+ACK)响应客户端后,到客户端的ACK报文到达服务器之前,则一直保留在半连接状态中;
  
在Linux内核2.2之前,backlog大小包括半连接状态和全连接状态两种队列大小,

在Linux内核2.2之后,分离为两个backlog来分别限制半连接(SYN_RCVD状态)队列大小和全连接(ESTABLISHED状态)队列大小

SYN queue 队列长度由 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,默认为2048。

Accept queue 队列长度由 /proc/sys/net/core/somaxconn 和使用listen函数时传入的参数,二者取最小值。默认为
128

accept函数

服务器在调用accept函数后,会阻塞监听socket,等待客户端连接,并返回一个全新的描述符fd,代表与客户端的tcp连接

原型

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
/* 成功返回一个用于连接的描述符,失败返回-1 */

参数sockfd: 服务器开始调用socket()函数生成的,称为监听socket描述字;
参数*addr: 用于返回客户端的协议地址,这个地址里包含有客户端的IP和端口信息等,结构体与bind中的一致
参数addrlen: 返回客户端协议地址的长度

accept函数的返回值是由内核自动生成的一个全新的描述字(fd),代表与返回客户的TCP连接。如果想发送数据给该客户端,则我们可以调用write()等函数往该fd里写内容即可;而如果想从该客户端读内容则调用read()等函数从该fd里读数据即可。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个新的socket描述字,当服务器完成了对某个客户的服务,就应当把该客户端相应的的socket描述字关闭。

connect函数

tcp客户端在创建socket后,使用connect来连接服务器
这两个文件描述符(客户端connect的fd和服务器端accept返回的fd)就可以实现客户端和服务器端的相互通信。

原型

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
/* 成功返回0,失败返回-1 */

sockfd: 客户端的socket()创建的描述字
addr: 要连接的服务器的socket地址结构,这里面包含有服务器的IP地址和端口等信息,和bind的一致
addrlen: socket地址的长度

实例

struct socket_in   sockaddr_in;
int port = 12345;
char *ip =192.168.1.1memset(&sockaddr_in,0,sizeof(sockaddr_in));

sockaddr_in.sin_family = AF_INET;
sockaddr_in.sin_port = htons(port);
inet_aton(ip, &sockaddr_in,sin_addr);

connect(socket_fd, (struct sockaddr *)sockaddr_in, sizeof(sockaddr_in));

通信函数 read 和 write

以上的函数已足够服务器和客户端建立tcp连接
接下来我们介绍用于通信的函数

 ssize_t read(int fd, void *buf, size_t count);
 ssize_t write(int fd, const void *buf, size_t count);
 ssize_t send(int sockfd, const void *buf, size_t len, int flags);
 ssize_t recv(int sockfd, void *buf, size_t len, int flags);
 ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr,
socklen_t addrlen);
 ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t
*addrlen);
 ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
 ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

其中read 和 write用法与文件io中的一致,用法如下
UNIX环境编程(c语言)–文件I/O-文件共享

其他函数也不再一一介绍,用法大同小异,详细用法可以man手册查看

关闭通信close 和 shutdown

使用close关闭通信,就和文件io中的用法一样,详情看上面那个链接文件io的文章

如果对socket fd调用close()则会触发该TCP连接断开的四路握手,有些时候我们需要数据发送出去并到达对方之后才能关闭
socket套接字,则可以调用shutdown()函数来半关闭套接字:

int shutdown(int sockfd, int how);

如果how的值为 SHUT_RD 则该套接字不可再读入数据了; 如果how的值为 SHUT_WR 则该套接字不可再发送数据了; 如
果how的值为 SHUT_RDWR 则该套接字既不可以读,也不可以写数据了

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuanFuXinCSDN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值