IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> linux系统实现CS客户端服务器模型 -> 正文阅读

[网络协议]linux系统实现CS客户端服务器模型

linux下借助于套接字socket实现cs模型

下图展示的是OSI七层模型以及合并后更为实用的四层模型,其中我们常说的套接字socket编程基于传输层的,下面将介绍linux系统下实现一个简单的cs模型。

TCP/ IP

服务器端Server.c

创建套接字

进行套接字通信,那么最开始是要自己创建一个套接字,在linux系统中有这么个特点:一切皆文件,对于文件的操作都离不开一个fd(file descriptor)文件描述符,套接字也不例外,首先看看linux中提供的socket函数(查看linux开发手册):

SOCKET(2)                  Linux Programmer's Manual                 SOCKET(2)
NAME
       socket - create an endpoint for communication
SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>
       int socket(int domain, int type, int protocol);
       
DESCRIPTION
       socket()  creates  an endpoint for communication and returns a descriptor.
       The domain argument specifies a communication domain; this selects  the
       protocol  family  which will be used for communication.  These families
       are  defined  in  <sys/socket.h>.   The  currently  understood  formats

RETURN VALUE
       On  success,  a  file  descriptor  for  the new socket is returned.  On
       error, -1 is returned, and errno is set appropriately.

简言之,socket()函数需要传入三个参数:domain、type、protocol

int socket(int domain, int type, int protocol);

其中第一个参数用于指明我们所用到的协议类型,这里为了实现稳定的tcp通信,传入AF_INET即可,表示我们选用IPv4网络协议。type为数据传输方式/套接字类型,常见的有SOCK_STREAM(流式套接字/稳定有连接的套接字)、SOCK_DGRAM(数据报套接字/无连接的套接字), 这里使用流式套接字SOCK_STREAM, 最后一个参数确定的是选用的协议,在此,TCP((Transfer Control Protocol)是流式套接字中的代表,可以传入 “0” 表示我们选用默认协议TCP,另外,数据报套接字代表协议是UDP(User Data Protocol)

int tcp_socket = socket(AF_INET, SOCK_STREAM, 0);  //创建TCP套接字
int udp_socket = socket(AF_INET, SOCK_DGRAM, 0);  //创建UDP套接字

当然也可以使用宏IPPROTO_TCP显式指明使用TCP协议:

int tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);  //IPPROTO_TCP表示TCP协议

函数返回值为int类型,

2.绑定套接字

刚创建的socket只是一个确定了使用具体协议的空壳,要与计算机中某个进程进行通信还需要对套接字进行信息绑定,使用bind函数:

BIND(2)                    Linux Programmer's Manual                   BIND(2)

NAME
       bind - bind a name to a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

DESCRIPTION
       When  a  socket  is  created  with socket(2), it exists in a name space
       (address family) but has no address assigned to it.  bind() assigns the
       address  specified  by  addr  to  the  socket  referred  to by the file
       descriptor sockfd.  addrlen  specifies  the  size,  in  bytes,  of  the
       address structure pointed to by addr.  Traditionally, this operation is
       called “assigning a name to a socket”.

sock为调用socket函数后返回的文件描述符,addr为sockaddr结构体变量的指针 addlen为addr变量的大小,可由sizeof()运算符计算得出。

int bind(int sock, struct sockaddr* addr, socklen_t addrlen);

在linux系统中的这些函数参数有这样的规律:

  • 参数前有const修饰表示参数为只读传参,也就是所谓传入参数,与之相对的就是传入传出参数。
  • 参数名后带有s,如: int* buffers,基本上(话不能说太满OvO~)表示传入的参数为一个数组。
  • 浏览一下 struct sockaddr 的结构信息:
 struct sockaddr {
       sa_family_t sa_family;
       char        sa_data[14];
};

在此处需要注意的是不能直接使用结构体struct sockaddr, 因为需要用IP地址和端口号与套接字绑定,对于端口号存在存储差异,在网络中端口号是大端存储,而本地端口号使用小端存储,什么是大端小端,有什么差异?
因此我们使用端口号之前必须通过转换,IP存储的格式为点分十进制,IPv4占用四个字节,每个字节对应的内容我们都需要写入到
sa_family参数需要填写的就是所使用的网络协议族: IPv4、IPv6,填入对应的宏即可(IPv4对应AF_INET,IPv6对应AF_INET6)。
第二个参数为一个数组sa_data[14];
我们在编写程序时并不使用struct sockaddr, 而是用sockaddr_in 代替它,
下图是 sockaddr 与 sockaddr_in 的对比(括号中的数字表示所占用的字节数):
在这里插入图片描述
sockaddr 和 sockaddr_in 的长度相同,都是16字节,只是将IP地址和端口号合并到一起,用一个成员 sa_data 表示。要想给 sa_data 赋值,必须同时指明IP地址和端口号,例如”127.0.0.1:80“,遗憾的是,没有相关函数将这个字符串转换成需要的形式,也就很难给 sockaddr 类型的变量赋值,所以使用 sockaddr_in 来代替。这两个结构体的长度相同,强制转换类型时不会丢失字节,也没有多余的字节。
可以认为,sockaddr 是一种通用的结构体,可以用来保存多种类型的IP地址和端口号,而 sockaddr_in 是专门用来保存 IPv4 地址的结构体。
正是由于通用结构体 sockaddr 使用不便,才针对不同的地址类型定义了不同的结构体。

3.让套接字进入监听状态并相应客户端请求

对于服务器端程序,使用 bind() 绑定套接字后,还需要使用 listen() 函数让套接字进入被动监听状态,再调用 accept() 函数,就可以随时响应客户端的请求了。

listen()函数
int listen(int sock, int backlog);  //Linux

listen函数用法较为简单,传入之前创建好的套接字以及一个backlog数值,第一个参数表示需要进入监听状态的套接字,第二个参数确定了请求队列的最大长度。

请求队列:当套接字在处理客户端请求时,可能会有新的请求进来,这时候就会按照顺序排在请求队列中等待被处理,当队列满时客户端提出请求会收到 ECONNREFUSED 错误。
注意:listen() 只是让套接字处于监听状态,并没有接收请求。接收请求需要使用 accept() 函数。

accept()函数
ACCEPT(2)              Linux Programmer's Manual              ACCEPT(2)

NAME
       accept, accept4 - accept a connection on a socket

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

       #define _GNU_SOURCE             /* See feature_test_macros(7) */
       #include <sys/socket.h>

       int accept4(int sockfd, struct sockaddr *addr,
                   socklen_t *addrlen, int flags);

DESCRIPTION
       The  accept()  system  call is used with connection-based socket
       types (SOCK_STREAM, SOCK_SEQPACKET).  It extracts the first con‐
       nection request on the queue of pending connections for the lis‐
       tening socket, sockfd,  creates  a  new  connected  socket,  and
       returns  a  new  file  descriptor referring to that socket.  The
       newly created socket is not in the listening state.  The  origi‐
       nal socket sockfd is unaffected by this call.

套接字进入监听状态后就可以处理客户端提出的请求了,accept()系统调用与基于连接的套接字一起使用(如SOCK_STREAM流式套接字、SOCK_SEQPACKET)。它提取第一个挂起连接队列上的连接请求, 创建了一个新的已连接的套接字,然后返回引用该套接字的新文件描述符。这个新创建的套接字未处于监听状态。原始套接字sockfd不受此调用影响。

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

addr 保存了客户端的IP地址和端口号,而 sock 是服务器端的套接字,addrlen是参数addr的占用空间大小

	//接收客户端的请求
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size = sizeof(clnt_addr);
	int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, clnt_addr_size);
4.向套接字写入内容

accept() 返回一个新的套接字来和客户端通信,注意区分: 后面和客户端通信时,要使用这个新生成的套接字,而不是原来服务器端的套接字。
演示一下使用套接字向客户端写入信息:

	char str[] = "bincode.blog.csdn.net";
	write(clnt_sock, str, sizeof(str))

最后需要说明的是:listen() 只是让套接字进入监听状态,并没有真正接收客户端请求,listen() 后面的代码会继续执行,直到遇到 accept()。accept() 会阻塞程序执行(后面代码不能被执行),直到有新的请求到来。

完整程序
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
//该ip为本机地址
#define IP "127.0.0.1"
#define PORT 1234
int main(){
	//af(address family)网络格式为IPv4 使用默认的流式协议 tcp 
 	//socket用于创建套接字,确定套接字的各种属性,服务器需要使用bind函数将套接字与特定的ip地址和端口绑定起来,只有这样,流经该ip地址和端口的数据才能交给套接字处理,类似的,客户端也需要用connect建立连接
	int serv_sock = socket(AF_INET, SOCK_STREAM, 0);	
	//创建sockaddr_in结构体变量
	struct sockaddr_in serv_addr;
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;//使用IPv4地址
	serv_addr.sin_addr.s_addr = inet_addr(IP);//具体的IP地址 将ip地址的格式转换为数值
	serv_addr.sin_port = htons(PORT);
	//将套接字和IP 端口绑定
	bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
	//监听套接字
	listen(serv_sock, 20);
	//接收客户端的请求
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size = sizeof(clnt_addr);
	int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, clnt_addr_size);
	char str[] = "bincode.blog.csdn.net";
	write(clnt_sock, str, sizeof(str));
	close(clnt_sock);
	close(serv_sock);
	return 0;

客户端Client.c

客户端Client.c与服务器相比减少了listen函数以及accept函数的使用。使用到的connect()与bind()使用方式是一致的,所以这里不作详细介绍了。直接贴上代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
//该ip为本机地址
#define IP "127.0.0.1"
#define PORT 1234
int main(){
	int clnt_sock = socket(AF_INET, SOCK_STREAM, 0);	
	//创建sockaddr_in结构体变量
	struct sockaddr_in clnt_addr;
	memset(&clnt_addr, 0, sizeof(clnt_addr));
	clnt_addr.sin_family = AF_INET;//使用IPv4地址
	clnt_addr.sin_addr.s_addr = inet_addr(IP);//具体的IP地址
	clnt_addr.sin_port = htons(PORT);
	//将套接字与服务器套接字连接起来
	connect(clnt_sock, (struct sockaddr*)&clnt_addr, sizeof(clnt_addr));

	//接收从服务端返回的信息
	char buffer[100];
	int rr = read(clnt_sock, buffer, 100);//读取100个字节出来
	printf("%s\n",buffer);
	return 0;
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2021-07-17 12:17:20  更:2021-07-17 12:19:23 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 17:52:57-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码