网络编程(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 模型代码

相关推荐
Qimooidea30 分钟前
祁木 CAD Translator 全套功能 & 官网入口汇总
运维·服务器
梦想的颜色1 小时前
Docker 容器化本地部署 Claude Code 完整硬核教程|隔离沙盒 + 国产模型直连 + 持久化 Skill
运维·容器·ai 工程化·沙盒隔离·docker 本地部署·claude code硬核实战·国内国产模型接入
wbs_scy1 小时前
仿 muduo 高并发服务器项目:从 timerfd 到时间轮实现定时任务机制
linux·服务器·c++
小趴菜え1 小时前
升级Ubuntu20.04版本至Ubuntu22.04
linux·运维
渣渣盟2 小时前
Docker 运维常用命令手册(含扩容与实战)
运维·docker·容器
小羊Yveesss2 小时前
2026年外贸建站服务器怎么选?访问速度、稳定性和海外收录怎么判断
大数据·运维·服务器
Darkwanderor2 小时前
Linux进程优先级操作
linux·运维·c语言·c++
小五传输3 小时前
内外网文件交换系统产品推荐 解决隔离网络文件交换5类难题
大数据·运维·安全
AOwhisky3 小时前
Linux(CentOS)系统管理入门笔记(第一期)——从 Multics 到主流发行版
linux·运维·笔记·centos·云计算
从零开始的代码生活_3 小时前
Linux epoll 多路转接详解
linux·运维·网络·后端·tcp/ip·计算机网络·php