网络编程(TCP连接)

一、服务器和客户端(单对单)

1、TCP服务器创建流程

1)socket(创建服务器套接字)

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建不同类型的套接字用文件描述符
参数 domain:套接字所依赖的网络介质
    如果是 ipv4 就填入 AF_INET
    如果是 ipv6 就填入 AF_INET6
    如果是 域套接字 就填入 AF_LOCAL / AF_UNIX
参数 type:选择套接字的类型
    SCOK_STREAM:字节流套接字传输数据,连续,可靠,双向,数据量大
    SOCL_DGRAM:数据包套接字传输数据,不连续,不可靠,有长度要求,双向
参数 protocol:选择套接字所依赖的通信协议
    0:自动匹配,会根据参数 type 和参数 protocol 自动选择合适的通信协议
返回值:
    返回创建套接字文件描述符

一般来说:
    AF_INET + SOCK_STREAM + 0 ,最终创建的是 TCP 协议的套接字
    AF_INET + SOCK_DGRAM  + 0 ,最终创建的是 UDP 协议的套接字 

2)定义struct sockaddr_in类型结构体

若为 ipv6 型的 ip地址 则使用结构体 struct sockaddr_in6

cs 复制代码
struct sockaddr_in {
  __kernel_sa_family_t  sin_family; /* 依赖的网络介质       */
  __be16        sin_port;           /* port端口号           */
  struct in_addr    sin_addr;       /* ip地址               */
};

3)使用bind函数命名套接字

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:往套接字中写入 ip地址 和 port端口号 (该操作被称为为套接字命名)
参数 sockfd:填写 套接字用文件描述法
参数 addr:通用套接字结构体指针,需要提前准备 struct sockaddr_in 类型的结构体
参数 addrlen:参数 addr 的字节长度

4)listen(设置服务器监听列表)

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:将想要连接服务器的客户端加入监听列表,等待客服务器与其连接
参数 sockfd:套接字文件描述符
参数 backlog:监听列表的长度

监听列表:
    所有等待服务器连接的客户端,都会在监听列表等待
    服务器连接客户端后,会将连接的客户端从监听列表移除
    若服务器只监听不连接,监听列表被塞满,则不会添加新的客户端到列表

5)accept(接受监听列表中客户端的连接)

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:连接监听列表第一个客户端(阻塞函数)
参数 sockfd:接受客户端连接的服务器的套接字
参数 addr:结构体接收已连接的客户端套接字文件中的ip地址和port端口号,若填NULL则不接受连接
参数 addrlen:结构体addr的字节长度
返回值:返回连接的客户端的套接字,没有客户端可连接就阻塞

6)recv | read(接收数据)

cs 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:通过套接字,读取套接字中ip地址所发送的数据
参数 sockfd:由socket创建的套接字文件描述符(要读取的目标)
参数 buf:将读取到的数据存入buf所指向的连续地址(所指向的数组)
参数 len:所读取数据的字节长度
参数 flags:设置函数的状态 阻塞 / 非阻塞
    0    :默认阻塞,没有接收到数据就阻塞
    MSG_DONTWAIT:非阻塞,没有读取到数据直接返回 0 
返回值:
    阻塞模式:
        返回接收到数据的字节数,若套接字损坏 返回 -1
        若服务器与客户端连接断开,则有阻塞函数变为非阻塞函数,并返回 0
    非阻塞模式:
        返回接收到数据的字节数,若服务器与客户端断开,返回 -1
        未接收到参数,则返回 0
=====================================================================
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
用法基本一致,IO篇也有详细介绍

7)服务器代码

2、TCP客户端创建流程

1)socket(创建服务器套接字)

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
功能:创建不同类型的套接字用文件描述符
参数 domain:套接字所依赖的网络介质
    如果是 ipv4 就填入 AF_INET
    如果是 ipv6 就填入 AF_INET6
    如果是 域套接字 就填入 AF_LOCAL / AF_UNIX
参数 type:选择套接字的类型
    SCOK_STREAM:字节流套接字传输数据,连续,可靠,双向,数据量大
    SOCL_DGRAM:数据包套接字传输数据,不连续,不可靠,有长度要求,双向
参数 protocol:选择套接字所依赖的通信协议
    0:自动匹配,会根据参数 type 和参数 protocol 自动选择合适的通信协议
返回值:
    返回创建套接字文件描述符

一般来说:
    AF_INET + SOCK_STREAM + 0 ,最终创建的是 TCP 协议的套接字
    AF_INET + SOCK_DGRAM  + 0 ,最终创建的是 UDP 协议的套接字 

2)定义struct sockaddr_in类型结构体

cs 复制代码
struct sockaddr_in {
  __kernel_sa_family_t  sin_family; /* 依赖的网络介质       */
  __be16        sin_port;           /* port端口号           */
  struct in_addr    sin_addr;       /* ip地址               */
};

3)使用bind函数命名套接字

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:往套接字中写入 ip地址 和 port端口号 (该操作被称为为套接字命名)
参数 sockfd:填写 套接字用文件描述法
参数 addr:通用套接字结构体指针,需要提前准备 struct sockaddr_in 类型的结构体
参数 addrlen:参数 addr 的字节长度

4)connect(通过套接字申请连接服务器)

cs 复制代码
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
功能:连接套接字文件描述符指向的服务器
参数 sockfd:要连接的服务器的套接字文件描述符
参数 addr:结构体存储要连接的服务器的ip地址和port端口号
参数 addrlen:结构体addr的长度
返回值:
    成功连接返回 0 ,失败返回 -1

5)send | write(发送数据)

cs 复制代码
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
功能:通过套接字,向套接字中指向的ip地址发送数据
参数 sockfd:填入 socket创建的套接字文件描述符(发送的目标)
参数 buf:填入 要发送的数据的地址
参数 len:填入 要发送的字节的长度
参数 flag:设置函数的状态 阻塞 / 非阻塞
    0    :默认阻塞,发送数据给目标,目标的接收区满了,就会发送阻塞
    MSG_DONTWAIT:非阻塞,发送数据给目标,接收区满了,丢弃新发送的数据
=======================================================================
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
用法基本一致,IO篇也有详细介绍

6)客户端代码

3、TCP存在的问题

1)TCP协议的执行逻辑

将短时间内连续发送的数据存储在一个1500字节的缓存区,每次发送数据,其实时固定发送了1500个字节,这样的好处时大大的提高了发送数据的效率。这也出现了两个问题

2)粘包问题和截断问题

  1. 短时间内连续发送的数据(小于1500字节),会被粘连在一起发送(接收端要拆分)

  2. 如果要发送的数据总大小超过1500字节,那么超出1500字节的部分,会被截断,留着下次发送

3)解决粘包问题

cs 复制代码
#include <my_head.h>

enum Type{
    TYPE_REGIST,
    TYPE_LOGIN
};

typedef struct Pack{
	int size;
    enum Type type;
	char buf[4096];
	int used;// 记录一下当前pack包的buf用了几个字节
}pack_t;


// 写一个函数,读取pack包里面的数据
// 参数 1:等待读取的pack包
char** read_data_from_pack(pack_t* pack){
	// packsize 是整个包的总大小
	char* buf = pack->buf;
	
	// 创建一个堆空间数组,用于向外返回数据
	//char** list = malloc(sizeof(char*)*20);
	char** list = calloc(20,sizeof(char*));// 申请堆空间变初始化
	int i = 0;
	
	int readed_size = 0;// 用来记录buf已经读取了多少个字节

	while(1){
		short size = *(short*)(buf+readed_size);
		//printf("size = %d\n",size);
		if(size == 0){
			break;
		}

		readed_size += 2;

		char temp[size+1];
		memset(temp,0,size+1);
		strncpy(temp,buf+readed_size,size);

		readed_size += size;

		//printf("temp = %s\n",temp);

		list[i] = calloc(1,size+1);
		strcpy(list[i],temp);
		i++;
	}
	return list;
}

int main(int argc, const char *argv[])
{
	if(argc < 2){
		printf("请输入端口号\n");
		return 1;
	}
	short port = atoi(argv[1]);
	// "abc123" -> 0
	int server = socket(AF_INET,SOCK_STREAM,0);

	struct sockaddr_in addr = {0};
	addr.sin_family = AF_INET;	
	addr.sin_port = htons(port);
	addr.sin_addr.s_addr = inet_addr("0.0.0.0");

	if(bind(server,(struct sockaddr*)&addr,sizeof(addr)) == -1){
		perror("bind");
		return 1;
	}

	listen(server,10);

	struct sockaddr_in client_addr;
    int client_len = sizeof(client_addr);

    int client = accept(server,(struct sockaddr*)&client_addr,&client_len);
	printf("有客户端连接\n");

	while(1){
	
		int size = 0;
		int res = read(client,&size,4);
		// 先读取前4个字节的目的在于:知道一下客户端发来的包到底多大
		if(res == 0){
			printf("客户端断开连接\n");
			return 0;
		}

		// 到此为止,我们已经知道了客户端发来的数据包,总共size个字节
		pack_t pack = {0};
		pack.size = size;
		res = read(client,(char*)&pack+4,size-4);

		//printf("packsize = %d\n",size);
		//printf("type = %d\n",pack.type);
		// (char*)&pack+4 含义为:将读取的数据,码放在pack首地址偏移4个字节的位置处
		// size - 4含义为:整个数据包只有size个字节,前面74行已经读取了4个字节,现在只剩 size - 4个字节可读
		
		// 到目前为止,我们只是将客户端发来的数据,读取到了pack数据包里面而已
		// 还没有解包
		
		// 开始解包
		char** list = read_data_from_pack(&pack);
		printf("账号 = %s\n",list[0]);
		printf("密码 = %s\n",list[1]);
	}
	return 0;
}

二、服务器和多客户端(单对多)

1、多线程服务器

2、多进程服务器

3、多路文件IO

4、select模型

1)创建 select 模型的监视列表

2)操作 监视列表

3)select 模型的监视函数

4)select 模型代码

5)select 模型的优缺点

5、poll 模型

1)创建 poll 的监视列表

2)操作监视列表

3)poll 模型监视函数

4)poll 模型代码

相关推荐
Gold Steps.5 分钟前
基于 Gitlab、Jenkins与Jenkins分布式、SonarQube 、Nexus 的 CiCd 全流程打造
运维·ci/cd·gitlab·jenkins
Ruimin051937 分钟前
LSV负载均衡
linux·运维·服务器·负载均衡·lvs
AuroraDPY1 小时前
Linux 环境变量
linux·运维·服务器
Ronin3052 小时前
【Linux系统】进程切换 | 进程调度——O(1)调度队列
linux·运维·服务器·ubuntu
Kevin Wang7273 小时前
Ubuntu服务器安装Miniconda
linux·服务器·ubuntu
菜萝卜子5 小时前
【Linux】Ubuntu22.04安装zabbix
linux·运维·zabbix
电信2301杨臣5 小时前
Imx6ull用网线与电脑连接
运维·服务器·网络
艾伦_耶格宇5 小时前
【DOCKER】-4 dockerfile镜像管理
运维·docker·容器