网络编程——套接字、创建服务器、创建客户端

[复制链接]
发表于 2025-10-12 17:05:04 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

一、套接字

1.1什么是套接字

套接字文件,原来就是一个和管道文件雷同,用来实现进程间通讯的一个文件
既然有了管道文件,当时为什么还要开辟套接字文件,去实现进程的通讯
由于管道文件是半双工模式的
套接字文件是全双工模式的
但是厥后,有了OSI 7层模子以及TCP协议 4层模子之后
套接字文件就进化出了新的功能:
套接字文件中允许存放 ip 和 port 以实现数据的跨主机传输
原先只能实现进程间通讯的套接字,就有了一个新的名字:域套接字
1.2 怎样创建一个套接字文件:socket
  1. 函数原型:int socket(int domain, int type, int protocol);
  2. 功能描述:创建一个套接字文件,参数不同,功能类型就会不同
  3. 参数描述:
  4.     参数 domain:网络介质,常用的有以下几种
  5.         AF_UNIX,AF_LOCAL : 本地通信协议,其实就是给域套接字用的
  6.         AF_INET:ipv4 地址协议
  7.         AF_INET6:ipv6 地址协议
  8.         最常用的就是: AF_INET
  9.     参数 type:套接字类型,有以下2个常用类型
  10.         SOCK_STREAM:提供一种有序的、可靠的、全双工的、基于连接的字节流套接字
  11.             这个描述一看就知道,tcp协议用的就是这种套接字
  12.         SOCK_DGRAM:提供一种数据报文形式的,非连接的,不可靠的,发送的数据有最大长度限制的套接字
  13.             这个描述一看就知道,udp协议使用的是这种套接字
  14.     参数 protocol: 一般直接写0
  15.         表示:网络通信协议会根据套接字的类型自动选取
  16.             如果套接字类型为 SOCK_STREAM,protocol 为0,则自动选用tcp协议
  17.             如果套接字类型为 SOCK_DGRAM,protocol 为0,则自动选用udp协议
  18. 返回值:成功创建套接字,返回套接字的描述符,失败返回-1
  19.     套接字描述符和文件描述一回事儿,遵循最小未使用原则
复制代码
1.3 怎样向套接字文件中写入ip 和 port

套接字写入ip和port实在就是明确一下ip和port到底是多少
之前讲过,服务器和客户端都必要明确ip和port
也就是说,服务器套接字和客户端套接字,都必要将ip和port写入套接字内里
1.3.1 先要准备一个地点信息布局体:struct sockaddr_in
  1. 该结构体结构如下:
  2. struct sockaddr_in{
  3.     unsigned short sin_family; // 这是一个标记位,固定写 AF_INET
  4.     unsigned short sin_port; // 端口号
  5.     struct in_addr sin_addr;// 一个结构体,结构如下
  6.         struct in_addr{
  7.             unsigned int s_addr; // 用来存放ip地址的变量        
  8.         }
  9.     unsigned char zero[8]; // 没有实际意义,完全为了字节对齐填充大小
  10. }
  11. 实例:准备将 "192.168.1.1" 和 8888 端口写入结构体中,为未来写入套接字中做准备
  12.     struct sockaddr_in addr = {0};
  13.     addr.sin_family = AF_INET;
  14.     addr.sin_port = htons(8888); // 因为本地字节序和网络字节序不一定一致,所以端口号在进入网络之前,一定要转换成大端字节序
  15.     addr.sin_addr.s_addr = inet_addr("192.168.1.1");
复制代码
1.3.2 将准备好的存有ip和port的布局体写入套接字中
  1. 函数原型:int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  2. 功能描述:将写有ip和port地址信息结构体,写入套接中
  3. 参数描述:
  4.     参数 sockfd:准备写入ip和port的套接字
  5.     参数 addr:写有ip和port的结构体地址,该结构体如下
  6.         struct sockaddr {
  7.             unsigned short sa_family;  // 固定写 AF_INET
  8.             char        sa_data[14]; // 这个14个字节的内存里面,就是用来存放 ip 和 port的
  9.         }
  10.         如果使用这个结构体去存放ip和port的话,代码如下
  11.         
  12.     参数 addrlen:就是第二个参数 addr 的实际占用长度
  13. 返回值:成功返回0,失败返回-1。bind函数非常容易报错,只要绑定的端口号已经被用了,就会报错
  14.         
  15. 这个 struct sockaddr 这个结构体我们成为通用地址结构体
  16. 事实上有2个结构体会通过我们所介绍的地址上的0101直接赋值的形式转换成 struct sockaddr 这个结构体
  17. struct sockaddr_in
  18. struct sockaddr_un
  19. 调用形式
  20.     struct sockaddr_in addr = {0};
  21.     addr.sin_family = AF_INET;
  22.     addr.sin_port = htons(8888); // 因为本地字节序和网络字节序不一定一致,所以端口号在进入网络之前,一定要转换成大端字节序
  23.     addr.sin_addr.s_addr = inet_addr("192.168.1.1");
  24.     if(bind(server,(struct sockaddr*)&addr,sizeof(addr)) == -1){
  25.         perror("bind");   
  26.     }
复制代码
1.4 怎样利用套接字文件传输数据

1.4.1 直接利用 read 大概 write 函数

文件IO内里的read 和 write 怎么用,套接字的read和write就怎么用
不再先容
1.4.2 recv 和 send

这2个函数专门给套接字利用的,形貌符用不了
  1. 函数原型:ssize_t send(int sockfd, const void *buf, size_t len, int flags);
  2. 功能描述:通过套接字,将数据发送到网络中
  3. 参数描述:
  4.     前3个参数 sockfd、buf、len :功能和write一模一样
  5.     参数 flags:这个参数也是send 和 write 的区别所在
  6.         flags 可以设定 send 是一个阻塞型 IO 还是非阻塞型 IO
  7.             0:阻塞型IO
  8.             MSG_DONTWAIT:非阻塞型IO
  9.             send什么时候阻塞:发送数据目标地点,如果满了,则send会阻塞,直到目标地点产生新的空间为止、
  10. 所以从我们的角度来看:send 和write 唯一的区别在于
  11.     send 自带设置阻塞和非阻塞的参数
  12.     write 不自带这个功能,默认阻塞
  13. 函数原型:ssize_t recv(int sockfd, void *buf, size_t len, int flags);
  14. 功能描述:通过套接字,从网络中读取数据
  15. 参数描述:
  16.     前3个参数 sockfd、buf、len :功能和read一模一样
  17.     参数 flags:这个参数也是send 和 write 的区别所在
  18.     flags 可以设定 send 是一个阻塞型 IO 还是非阻塞型 IO
  19.         0:阻塞型IO
  20.         MSG_DONTWAIT:非阻塞型IO   
  21. 所以从我们的角度来看:recv 和 read 唯一区别在于
  22.     recv 自带设置阻塞和非阻塞的参数
  23.     read 不自带,默认阻塞
复制代码
1.5 怎样将恣意一个壅闭型IO函数,设置成非壅闭的
  1. 函数原型:int fcntl(int fd, int cmd, ... /* arg */ );
  2. 功能描述:该函数有多个功能,到底执行哪个功能由 cmd 的值决定
  3. 参数描述:
  4.     参数 cmd :
  5.          F_GETFL:获取 fd 描述符的 flag
  6.          F_SETFL:设置 fd 描述符的 flag
  7.              当cmd选择F_SETFL的时候,需要传入第3个参数,第三个参数功能为 想要设置的具体的flag的值
  8.     到底什么是flag:其实就是open函数第2个参数 flag,也就是文件描述符属性
  9.         属性有:可读,可写,追加,清空,创建,非阻塞 等等等等
  10.         
  11. 以scanf为例:通过fcntl函数将标准输入流修改成非阻塞型IO
  12.     scanf 用的是 stdin 标准输入流,阻塞型IO
  13.     一旦将标准输入流修改成非阻塞型IO之后,scanf就不阻塞了
复制代码

二、创建服务器

2.1 服务器的搭建

2.1.1 创建服务器套接字:socket

2.1.2 提前准备ip和port:struct sockaddr_in addr

2.1.3 为服务器套接字绑定ip和port :bind

2.1.4 创建一个监听列表:listen

全部毗连服务器的客户端,都会被服务器加入到监听列表中去
  1. 函数原型:int listen(int sockfd, int backlog);
  2. 功能描述:创建监听列表并监听客户端连接,如果有客户端连接,则将该客户端加入监听列表中
  3. 参数描述:
  4.     参数 sockfd:哪个描述符需要监听并创建监听列表,一般就是服务器的描述符
  5.     参数 backlog:监听列表的大小
复制代码
2.1.5 从监听列表中,担当客户端的毗连:accept
  1. 函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  2. 功能描述:接受客户端的连接,并且获取该客户端的ip和port
  3. 参数描述:
  4.     参数 sockfd:哪个描述符接受客户端的连接,一般就是服务器
  5.     参数 addr:一个地址信息结构体的地址,该结构体中将会接受并存放新链接的客户端的ip和port
  6.     参数 addrlen:第二个参数的实际长度,但是注意这里是个指针
  7.    
  8. 返回值:成功接受客户端的连接之后,会返回该客户端的套接字
  9.     客户端套接字非常有用,服务器与特定的客户端进行通信的时候,必须使用该客户端套接字
  10.    
  11. 调用形式:
  12.     struct sockaddr_in client_addr = {0};
  13.     int len = sizeof(client_addr);
  14.     int client = accept(server,(struct sockaddr*)&client_addr,&len);
  15.    
  16. 当然,我们也可以仅接受客户端的连接,不需要知道客户端的ip和port
  17.     int client = accept(server,NULL,NULL);
  18.    
  19. 注意:accept 函数是一个阻塞型函数
  20.     也就是说,一旦调用accept函数就会立刻阻塞,直到有客户端连接为止
复制代码
2.1.6 服务器代码


三、创建客户端

3.1 客户端的搭建

3.1 创建客户端套接字 :socket

3.2 为客户端准备 ip 和 port :struct sockaddr_in addr

3.3 毗连服务器:connect
  1. 函数原型:int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
  2. 功能描述:根据提供的ip和port连接到指定服务器
  3. 参数描述:
  4.     参数 sockfd:客户端套接字
  5.     参数 addr:存有ip和port的一个通用地址信息结构体
  6.     参数 addrlen:第2个参数的实际长度
  7. 返回值:成功连接返回0,失败返回-1,因为存在失败的可能,所以记得判空
  8. 调用形式
  9.     struct sockaddr_in addr = {0};
  10.     addr.sin_family = AF_INET;
  11.     addr.sin_port = htons(8888); // 因为本地字节序和网络字节序不一定一致,所以端口号在进入网络之前,一定要转换成大端字节序
  12.     addr.sin_addr.s_addr = inet_addr("192.168.1.1");
  13.     if(connect(server,(struct sockaddr*)&addr,sizeof(addr)) == -1){
  14.         perror("bind");   
  15.     }
复制代码
3.4 客户端断开毗连

客户端一旦与服务器断开链接之后
服务器那边,通过客户端套接字读取客户端发送数据的read大概recv函数,就会从壅闭变成非壅闭
变成非壅闭之后,假如没有读取到数据,则会返回0
以是我们可以根据read大概recv的返回值来判定客户端是否断开毗连
3.5 客户端代码


四、TCP中的粘包征象

tcp协议为了进步发送的服从,会将短时间一连发送的小数据,当做一组数据同一发送
原理是:
tcp协议自己存在一个1500字节的缓存区,tcp协议每次write发送数据的时间,总是会发送1500个字节
假如发送了n组数据,这n组数据的时间隔断很短,而且数据总巨细没有凌驾1500个字节
那么TCP协议就会将这n组数据全都放到同一个1500字节的缓存区中去,同一发送
4.1 协议包

规定好一组数据的固定巨细,以及一组数据内里每个数据占据多少个字节
然后服务器和客户端,全都依照同样的规定实现数据的收发
如许的规定假如我们把它打包写成详细的代码,打包出来的结果我们就称为协议包
4.1.1 利用协议包的服务器


4.1.2 利用协议包的客户端



回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表