# 基本套接字编程

# IPv4 套接字地址结构

#include <netinet/in.h>
/* POSIX 规范只需要 3 个字段:sin_family, sin_port, sin_addr */
struct sockaddr_in{
    uint8_t sin_len;	       /* 长度字段,部分厂家支持,简化了长度可变套接字地址结构的处理 */
    sa_family_t sin_family;    /* 套接字地址结构的地址族 AF_INET */
    struct in_addr sin_addr;
    in_port_t sin_port;
    char sin_zero[sizeof (struct sockaddr) - sizeof (sa_family) - sizeof (in_port_t) - sizeof (struct in_addr)];
};
  • sin_family :地址协议族,此处使用 AF_INET

  • sin_addr :IPV4 地址(网络字节序)

    typedef uint32_t in_addr_t;
    struct in_addr{
        in_addr_t	s_addr;
    };

    sin_addr 定义为一个结构体而不仅仅是一个 in_addr_t 类型的无符号长整数,这是有历史原因的;早期的版本把 in_addr 结构定义为多种结构的联合,允许访问一个 IPV4 地址中的所有字节,或者访问它的 2 个 16 位值。这是为了适应地址被划分为 A、B 和 C 三类的时期,便于获取地址中适当的字节。然而随着子网划分技术的来临和无类地址编排的出现,各种地址类正在消失,这种联合也就不再需要了。

  • sin_port :16 位端口(网络字节序)

# IPv6 套接字地址结构

# 字节序转化函数

  • 内存中储存数据的方式有两种:大端字节序和小端字节序;两种格式都有系统使用,目前并没有统一的标准;我们把某个给定系统所用的字节序称为主机字节序;
  • 数据在网络中的传输方式称为网络字节序,通常采用大端存储的方式.
  • 由于一些历史原因和 POSIX 规范的规定,套接字地址结构中某些字段必须按照网络字节序的方式进行维护.

因此,在进行网络通信之前,套接字地址结构中的端口号、地址需要以网络字节序的方式存储.

# 端口 字节序转化函数

#include <netinet/in.h>
// 16 位端口 字节序转化函数
uint16_t htons(uint16_t host_port); 
uint16_t ntohs(uint16_t net_port);
// 32 位端口 字节序转化函数
uint32_t htonl(uint32_t host_addr);
uint32_t ntohl(uint32_t net_addr);

# 地址 字节序转化函数

  1. IPV4 专用
#include <arpa/inet.h>
//  将 strptr 所指字符串转化为一个 32 位的网络字节序二进制值,并通过 in_addr 来存储;若成功返回 1,否则返回 0.
int inet_aton(const char *strptr, struct in_addr *addrptr);
//  将 32 位网络字节序二进制 IPV4 地址转化为点分十进制字符串
char *inet_ntoa(struct in_addr *addrptr);
  1. IPV4/IPV6 通用(建议使用
#include <arpa/inet.h>
//  将 strptr 所指字符串转化为网络字节序,使用 addrptr 来接收,并通过 in_addr 来存储;若成功返回 1,否则返回 0.
int inet_pton(int family, const char *strptr, void *addrptr);
// 将 addrptr 网络字节序转化为点分十进制字符串,并存储在 strptr 中,若成功返回 strptr,否则返回 NULL
const char* inet_ntop(int family, const void *addrptr, char *strptr, int len);

inet_ntop 中的 len 参数指代目标存储区 ( strptr ) 大小,以免函数溢出调用者的缓冲区。如若 len 太小不足以容纳表达式格式结果 (包含末尾的 \0 ),则会返回 NULL . 同时 strptr 参数不能为 NULL ,调用者必须目标存储区分配相应的空间. len 的设置可以调用如下的宏定义:

#include <netinet/in.h>
#define INET_ADDRSTRLEN 16
#define INET6_ADDRSTRLEN 32

# socket 函数

#include <sys/socket.h>
int socket(int family, int type, int protocol);
/*
Description:
	domain: 协议域
		- AF_INET:IPV4 protocal
		- AF_INET6:IPV6 protocal
		- AF_LOCAL:Unix 域协议
		- AF_ROUTE: 路由套接字
		- AF_KEY: 秘钥套接字
	type: 套接字类型
		- SOCK_STREAM: 字节流套接字
		- SOCK_DGRAM: 数据报套接字
		- SOCK_SEQPACKET: 允许分组套接字
		- SOCK_RAW: 原始套接字
	protocal: 协议类型,或者设置为 0(会选择 domain 和 type 组合的系统默认值,但是并非所有的组合都是有效的)
		- IPPROTO_TCP:TCP 传输协议
		- IPPROTO_UPD:UDP 传输协议
		- IPPROTO_SCTP:SCTP 传输协议
Return Value:
	socket 函数成功则会返回一个小的非负整数值,它与文件描述符类似,称为套接字描述符 ssocket descriptor(sockfd)。失败则返回 - 1
*/
  • family :协议族
    1. AF_INET :IPv4 协议
    2. AF_INET6 :IPv6 协议
  • type 字段和 protocol

# 套接字编程

/*    IPV4 套接字地址     */
typedef unsigned short int sa_family_t;
struct sockaddr{
    sa_family_t sa_famliy;   /* 套接字地址结构的地址族 */
    char sa_data[14];        /* 端口号(2 字节)+ IP 地址(4 字节) + 填充(8 字节) */
};
/*
Synopsis:
    #include <sys/socket.h>
Additional:
	sockaddr 是通用的套接字地址结构,是早期的版本,同时与各种套接字函数都是适配的;当套接字作为一个参数参入套接字函数时,往往是以引用形式(也就是以指向该地址的指针)来传递。
	当使用便于赋值和使用的新型地址结构 sockaddr_in 时,使用套接字函数,传入套接字地址时,需要做一个强制类型转换变成通用的套接字地址结构的指针。
	struct sockaddr_in serv;
	(struct socketaddr *)& serv;
*/
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef uint16_t in_port_t;

# IPV6 套接字地址结构

/* IPV6 套接字地址结构 */
struct in6_addr{
    
};

# 值 - 结果参数

当往套接字函数传递一个套接字地址结构时,该结构总是以引用的形式来传递,也就是传递指向该结构的一个指针;其有两种传递方式

  • 从进程到内核传递套接字地址的函数有:connect、bind 和 sendto。这些函数的一个参数是指向某个套接字地址结构的指针,另外一个参数是指向该结构的整形大小;(即指针和指针所指内容的大小都传递给了内核,于是内核找到到底需要从进程复制多少数据进来)
  • 从内核到进程传递套接字地址的函数有:accept、recvfrom、getsockname 和 getpeername。这些函数的

# 字符操纵函数

#include <strings.h>
void bzero();
void bcopy();
int bcmp();
#include <string.h>
void *memsset();
void *memcpy();
int memcmp();

# TCP 套接字编程

# socket 函数


# connect 函数

typedef unsigned int socklen_t;
int connect(int sockfd, const struct sockaddr *serveraddr, socklen_t addrlen);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	sockfd: 客户端用于通信的套接字描述符;
	serveraddr: 服务器监听套接字
	addrlen: 服务器套接字地址结构长度
	客户在调用函数 connect 之前,不必非得调用 bind 函数;因为如果也需要的话,内核会确定源 IP 地址,并选择一个临时端口作为源端口
Return Value:
	若成功则返回 0,若出错则返回 - 1。若 connect 失败则该套接字不再可用,必须关闭。
*/

# bind 函数

int bind(int sockfd, const struct sockaddr *myaddr, socklen_t addrlen);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	把一个本地协议地址赋予一个套接字
	sockfd:
	myaddr:
	socklen_t:
Return Value:
*/

# listen 函数

int listen(int sockfd, int backlog);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	sockfd: 套接字描述符;当 socket 函数创建一个套接字时,它被假设为一个主动套接字,listen 函数把一个未连接的套接字转化为一个被动套接字,指示内核应该接受指向该套接字的连接请求;(调用 listen 导致套接字从 CLOSE 状态转换到 LISTEN 状态)
	backlog: 内核应该为相应套接字设置的最大排队连接请求
Return Value:
	若成功则返回 0,若失败返回 - 1;
*/

内核会为任何一个给定的监听套接字维护两个队列:

  • 未完成连接队列(incomplete connection queue)
  • 已完成连接队列(completed connection queue)

# accept 函数

int accept(int sockfd, struct sockaddr *clientaddr, socklen_t addrlen);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	accept 函数由 TCP 服务器调用,用于从已完成连接队列对头返回下一个已完成连接的客户端请求。如果已完成连接队列为空,那么进程被置于休眠状态。
	sockfd: 监听套接字描述符
	clientaddr: 客户进程地址结构
	addrlen: 客户进程协议地址长度
Return Value:
	若 accept 成功,那么其返回值是由内核自动生成的一个全新的套接字描述符,代表与所返回的客户的 TCP 连接。若服务器对客户协议地址不感兴趣,可以把 clientaddr 和 addrlen 设置为空指针。返回的已连接套接字在每次断开连接后需要关闭。若失败则返回 - 1;
*/

# recv 和 send 函数

recvsend 函数类似于标准的 readwrite 函数,不过需要一个额外的参数。

typedef long int ssize_t;
typedef long unsigned int size_t;
ssize_t recv(int sockfd, void *buff, size_t nbytes, int flags);
ssize_t send(int socket, void *buff, size_t nbytes, int flags);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	前三个参数等同于 read 和 write 的三个参数。flags 参数的值或为 0,或为如下字段:
		- MSG_DONTROUTE
		- MSG_DONTWAIT
		- MSG_OOB
		- MSG_PEEK
		- MSG_WAITALL
Return Value:
*/

# close 函数

int close(int sockfd);
/*
Synopsis:
	#include <unistd.h>
Description:
	sockfd: 待关闭的套接字描述符
Return Value:
	若关闭成功返回 0,若关闭失败返回 - 1;
*/

# 并发服务器

当服务器需要连接多个客户端时,可以使用迭代的方式,但是当服务一个客户请求可能花费较长时间时,我们并不希望整个服务器被单个客户长期占用,而是希望同时服务多个客户,这时需要在并发的服务器。

# TCP 客户 / 服务器程序实例

<img src="C:\Users\Value\AppData\Roaming\Typora\typora-user-images\image-20230522145338069.png" alt="image-20230522145338069" style="zoom:90%;" />

/* 服务端 */
//============================================================================
// Name        : learning_socket.cpp
// Author      : Value
// Version     :
// Copyright   : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <cstdio>
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
	/* 创建监听套接字描述符 */
	int listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	/* 监听套接字地址结构 */
	struct sockaddr_in listen_socket;
	listen_socket.sin_family = AF_INET;
	listen_socket.sin_port = htons(9999);
	inet_pton(AF_INET, "192.168.159.130", &listen_socket.sin_addr.s_addr);
	/* 绑定 IP 和 Port */
	int ret = bind(listen_socket_fd, (struct sockaddr*) &listen_socket, sizeof listen_socket);
	if(ret == -1){
		perror("bind error!");
		return -1;
	}
	/* 监听 */
	ret = listen(listen_socket_fd, 10);
	if(ret == -1){
		perror("listen error!");
		return -1;
	}
	printf("客户端启动中...\n");
	/* 等待客户端;连接 */
	struct sockaddr_in client_socket;
	unsigned int addrlen = sizeof client_socket;
	int client_socket_fd = accept(listen_socket_fd, (struct sockaddr*) &client_socket, &addrlen);
	if(client_socket_fd == -1){
		perror("accept error");
		return -1;
	}
	char ip[32];
	printf("客户端IP:%s, 端口:%d\n",
			inet_ntop(AF_INET, &client_socket.sin_addr.s_addr, ip, sizeof ip),
			ntohs(client_socket.sin_port));
	/* 通信 */
	while(true){
		/* 接收数据 */
		char buff[255];
		int len = recv(client_socket_fd, buff, sizeof buff, 0);
		if(len > 0){
			printf("received client messages:%s\n", buff);
			/* 发送消息 */
			send(client_socket_fd, buff, sizeof buff, 0);
		}else if(len == 0){
			printf("客户端断开了连接...");
			break;
		}else{
			perror("recv error!");
			break;
		}
	}
	close(client_socket_fd);
	close(listen_socket_fd);
	return 0;
}
/* 客户端 */
//============================================================================
// Name        : learning_client.cpp
// Author      : Value
// Version     :
// Copyright   : Your copyright notice
// Description : Hello World in C++, Ansi-style
//============================================================================
#include <cstdio>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
	/* 创建通信套接字描述符 */
	int client_socket_fd = socket(AF_INET, SOCK_STREAM, 0);
	if(client_socket_fd == -1){
		perror("socket error!\n");
		return -1;
	}
	/* 客户端可绑定固定 IP 和端口,也可以不绑定,操作系统自动选择空闲端口和本地 IP */
	struct sockaddr_in server_addr;
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(9999);
	inet_pton(AF_INET, "192.168.159.130", &server_addr.sin_addr.s_addr);
	/* 连接服务器 */
	int ret = connect(client_socket_fd, (struct sockaddr*) &server_addr, sizeof server_addr);
	if(ret == -1){
		perror("connect error!\n");
		return -1;
	}
	/* 通信 */
	int count = 1;
	while(true){
		printf("连接成功!\n");
		char buff[255];
		/* 发送数据 */
		sprintf(buff, "你好,我是客户端!发送给你%d\n",count ++ );
		send(client_socket_fd, buff, strlen(buff) + 1, 0);
		/* 接收数据 */
		memset(buff, 0, sizeof buff);
		int len = recv(client_socket_fd, buff, sizeof buff, 0);
		if(len > 0){
			printf("受到数据:%s\n", buff);
		}else if(len == 0){
			printf("服务器断开了连接...\n");
			break;
		}else{
			perror("recv error!\n");
			break;
		}
		sleep(1);
	}
	close(client_socket_fd);
	return 0;
}

# UDP 套接字编程

# recvfrom 和 sendto 函数

ssize_t recvfrom(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *from, socklen_t *addrlen);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	sockfd: 用于通信的套接字描述符
	buff: 指向读入缓冲区的指针
	nbytes:
	flags:
	from: 指向数据报发送者的协议地址的套接字地址结构
	addrlen: 套接字地址结构大小
	
*/
ssize_t sendto(int sockfd, void *buff, size_t nbytes, int flags, struct sockaddr *to, socklen_t addrlen);
/*
Synopsis:
	#include <sys/socket.h>
Description:
	sockfd: 用于通信的
	buff:
	nbytes:
	flags:
	to: 指向数据报接受者的协议地址(IP 地址以端口号)的套接字地址结构
	addrlen: 套接字地址结构大小
*/

# 高级 I/O 函数

# 套接字超时

# readv 和 writev 函数

这两个函数类似于 readwrite ,不过 readvwritev 允许单个系统嗲用读入或写入一个或多个缓冲区。

# recvmsg 和 sendmsg 函数

ssize_t recvmsg(int sockfd, struct sockaddr msghdr *msg, int flags);
ssize_t sendmsg(int sockfd, struct sockaddr msghdr *msg, int flags);
struct msghdr{
    void *msg_name;		/* Address to send to/receive from.  */
    socklen_t msg_namelen;	/* Length of address data.  */
    struct iovec *msg_iov;	/* Vector of data to send/receive into.  */
    size_t msg_iovlen;		/* Number of elements in the vector.  */
    void *msg_control;		/* Ancillary data (eg BSD filedesc passing). */
    size_t msg_controllen;	/* Ancillary data buffer length.
                   !! The type should be socklen_t but the
                   definition of the kernel is incompatible
                   with this.  */
    int msg_flags;		/* Flags on received message.  */
};
/*
Synopsis:
	#include <sys/socket.h>
Description:
	
Return Value:
*/

# 辅助数据

Edited on Views times

Give me a cup of [coffee]~( ̄▽ ̄)~*

Value WeChat Pay

WeChat Pay