C语言----详解socket通信

news/2025/2/12 5:01:30/

一:什么是socket

        刚接触socket的同学想必也知道socket的中文名,套接字,与其说是中文名倒不如说这是什么玩意,我们先不要管中文名的实际意义,我们先来了解一下什么是socket。

        我们上网产生的数据都是经过协议栈一层一层的封装然后经网卡发送到网络,经网络发送到服务端,然后服务端又是一层一层的解封装拿到自己想要的数据。

        对于协议栈都是集成在操作系统里,我们并不需要关心TCP,UDP等这些协议是如何实现的,我们关心的是我们的应用程序的数据能不能正常的发送出去和接收服务端发回来的数据。这就需要一个桥梁,一端连接操作系统的协议栈,一端连接用户的应用数据。socket就是这个桥梁。

        那我们再来理解一下中文名套接字,看了一圈我最赞同的解释是:套接指的是套接管,就是将两根水管套接起来的管子,然后“字”是此连接的数据标识,即一个WORD,所以套接字就是一个标识连接的数据体。

        那有的同学有疑问WORD是啥,在linux等系统中“套接字”对应“socket word”,所以“字”也就是对应“word”,这个“word”可能指储存socket的数据标识,因为端口号是两字节,就是一个WORD

        下边的图就很具体,没有上面那么抽象

        对于套接字的解释就到这了,实在编不下去了

二:socket通信流程

        如TCP的连接流程一样,TCP建链需要三次握手,TCP拆链需要四次挥手,socket通信也有自己的一套流程。

对于客户端:

1,创建一个用于通信的套接字(fd)

2,连接服务器,需要指定连接的服务器的IP 和 端口

3,建立连接成功,客户端和服务器建立连接通道

        1>可以发送数据

        2>可以接收数据

4,通信结束,断开连接

对于服务端:

1,创建一个用于监听的套接字

        1>监听:监听有客户端的连接

        2>套接字:这个套接字其实就是一个文件描述符

2,将这个监听文件描述符和本地的IP和端口绑定(IP和端口就是服务器的地址信息)

        1>客户端连接服务器的时候使用的就是这个IP和端口

3,设置监听,监听的fd开始工作

4,阻塞等待,当有客户端发起连接,解除阻塞,接受客户端的连接,会得到一个和客户端通信的套接字 (fd)

5,服务端和客户端通信

        1>接收数据

        2>发送数据

6,通信结束,断开连接

三:socket通信函数详解

1,socket()函数

int socket(int domain, int type, int protocol); - 功能:创建一个套接字 - 参数: - domain: 协议族 AF_INET : ipv4 AF_INET6 : ipv6 AF_UNIX, AF_LOCAL : 本地套接字通信(进程间通信) - type: 通信过程中使用的协议类型 SOCK_STREAM : 流式协议 SOCK_DGRAM : 报式协议 - protocol : 具体的一个协议。一般写0 - SOCK_STREAM : 流式协议默认使用 TCP - SOCK_DGRAM : 报式协议默认使用 UDP - 返回值: - 成功:返回文件描述符,操作的就是内核缓冲区。 - 失败:-1

注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()、listen()时系统会自动随机分配一个端口。

2,bind()函数

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); // socket命 名 - 功能:绑定,将fd 和本地的IP + 端口进行绑定 - 参数: - sockfd : 通过socket函数得到的文件描述符 - addr : 需要绑定的socket地址,这个地址封装了ip和端口号的信息 - addrlen : 第二个参数结构体占的内存大小 

        bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INET、AF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

3,listen()函数

int listen(int sockfd, int backlog);      // /proc/sys/net/core/somaxconn - 功能:监听这个socket上的连接 - 参数: - sockfd : 通过socket()函数得到的文件描述符 - backlog : 未连接的和已经连接的和的最大值, 5 

        作为服务端需要时刻监听是否有客户端发来的数据,服务端就是调用listen()来监听建立的socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

4,connect()函数

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);- 功能: 客户端连接服务器 - 参数: - sockfd : 用于通信的文件描述符 - addr : 客户端要连接的服务器的地址信息 - addrlen : 第二个参数的内存大小 - 返回值:成功 0, 失败 -1 

 客户端通过调用connect函数来建立与TCP服务器的连接

5,accept()函数

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); - 功能:接收客户端连接,默认是一个阻塞的函数,阻塞等待客户端连接 - 参数: - sockfd : 用于监听的文件描述符 - addr : 传出参数,记录了连接成功后客户端的地址信息(ip,port)- addrlen : 指定第二个参数的对应的内存大小 - 返回值: - 成功 :用于通信的文件描述符 - -1 : 失败 

        服务器侧在调用socket()、bind()、listen()之后,就会监听指定的socket地址了。客户端在调用socket()、connect()之后就建立了一条连接通道并发向服务端发送一个请求,服务器监听到这个请求之后,就会调用accept()函数取接收请求。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作

6,read()、write()函数

ssize_t write(int fd, const void *buf, size_t count);      // 写数据 
ssize_t read(int fd, void *buf, size_t count);                // 读数据

        read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节 数。失败时返回-1,并设置errno变量。在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是 全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示 网络连接出现了问题(对方已经关闭了连接)。

        网络I/O操作不止是read()/write()函数,下面几组也是的

read()/write()
recv()/send()
readv()/writev()
recvmsg()/sendmsg()
recvfrom()/sendto()

其申明如下:

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

7,close()函数

int close(int fd);

        close一个socket连接后会立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数

注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求

四:socket通信实战

客户端:

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sys/wait.h>#define SERVER_PORT 8888int main()
{//客户端只需要一个套接字文件描述符,用于和服务器通信int serverSocket;//描述服务器的socketstruct sockaddr_in serverAddr;char sendbuf[200]; //存储 发送的信息 char recvbuf[200]; //存储 接收到的信息 int iDataNum;/*********************************************************************//*                          1-创建客户端套接字                        *//*********************************************************************/if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0){perror("socket");return 1;}serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(SERVER_PORT);//指定服务器端的ip,本地测试:127.0.0.1//inet_addr()函数,将点分十进制IP转换成网络字节序IPserverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");/*********************************************************************//*                          2-连接服务端                              *//*********************************************************************/  if(connect(serverSocket, (struct sockaddr *)&serverAddr, sizeof(serverAddr)) < 0){perror("connect");return 1;}printf("连接到主机...\n");/*********************************************************************//*                          3-发送接收消息                            *//*********************************************************************/ while(1){printf("发送消息:");scanf("%s", sendbuf);printf("\n");send(serverSocket, sendbuf, strlen(sendbuf), 0); //向服务端发送消息if(strcmp(sendbuf, "quit") == 0) break;printf("读取消息:");recvbuf[0] = '\0';iDataNum = recv(serverSocket, recvbuf, 200, 0); //接收服务端发来的消息recvbuf[iDataNum] = '\0';printf("%s\n", recvbuf);}close(serverSocket);return 0;
}

服务端:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>#define SERVER_PORT 8888/*
监听后,一直处于accept阻塞状态,
直到有客户端连接,
当客户端如close后,断开与客户端的连接
*/int main()
{//调用socket函数返回的文件描述符int serverSocket;//声明两个套接字sockaddr_in结构体变量,分别表示客户端和服务器struct sockaddr_in server_addr;struct sockaddr_in clientAddr;int addr_len = sizeof(clientAddr);int clientSocket;char buffer[200]; //存储 发送和接收的信息 int iDataNum;/*********************************************************************//*                          1-创建服务端套接字                        *//*********************************************************************/if((serverSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0){perror("socket");return 1;}memset(&server_addr,0, sizeof(server_addr));//初始化服务器端的套接字,并用htons和htonl将端口和地址转成网络字节序server_addr.sin_family = AF_INET;server_addr.sin_port = htons(SERVER_PORT);//ip可是是本服务器的ip,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//对于bind,accept之类的函数,里面套接字参数都是需要强制转换成(struct sockaddr *)//bind三个参数:服务器端的套接字的文件描述符/*********************************************************************//*                          2-服务端绑定监听的IP和por                  *//*********************************************************************/  if(bind(serverSocket, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0){perror("connect");return 1;}/*********************************************************************//*                          3-服务端开始监听                          *//*********************************************************************/ if(listen(serverSocket, 5) < 0)//开启监听 ,第二个参数是最大监听数{perror("listen");return 1;}/*********************************************************************//*                          4-接收发送消息                            *//*********************************************************************/  printf("监听端口: %d\n", SERVER_PORT);//调用accept函数后,会进入阻塞状态//accept返回一个套接字的文件描述符,这样服务器端便有两个套接字的文件描述符,//serverSocket和client。//serverSocket仍然继续在监听状态,client则负责接收和发送数据//clientAddr是一个传出参数,accept返回时,传出客户端的地址和端口号//addr_len是一个传入-传出参数,传入的是调用者提供的缓冲区的clientAddr的长度,以避免缓冲区溢出。//传出的是客户端地址结构体的实际长度。//出错返回-1clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, (socklen_t*)&addr_len);if(clientSocket < 0){perror("accept");}printf("等待消息...\n");//inet_ntoa ip地址转换函数,将网络字节序IP转换为点分十进制IP//表达式:char *inet_ntoa (struct in_addr);printf("IP is %s\n", inet_ntoa(clientAddr.sin_addr)); //把来访问的客户端的IP地址打出来printf("Port is %d\n", htons(clientAddr.sin_port)); while(1){buffer[0] = '\0';iDataNum = recv(clientSocket, buffer, 1024, 0);if(iDataNum < 0){continue;}buffer[iDataNum] = '\0';if(strcmp(buffer, "quit") == 0) break;printf("收到消息: %s\n", buffer);printf("发送消息:");scanf("%s", buffer);send(clientSocket, buffer, strlen(buffer), 0); //服务端也向客户端发送消息 if(strcmp(buffer, "quit") == 0) break; //输入quit停止服务端程序 }close(clientSocket);close(serverSocket);return 0;}

 在Linux上用gcc编译:

gcc server.c -o server
gcc client.c -o client

先运行服务端:

再运行客户端:

客户端服务端收发消息:


http://www.ppmy.cn/news/1080837.html

相关文章

【2023百度之星备赛】码蹄集 BD202301 第五维度(二分 + 贪心)

题目 https://www.matiji.net/exam/brushquestion/3/4347/179CE77A7B772D15A8C00DD8198AAC74?from1 题目大意&#xff1a;给定n个科学家每秒获得理解力的大小和开始理解的时间&#xff0c;给定理解第五维度需要的总理解力。同时&#xff0c;可以随机让一位科学家在某一时刻消…

Nginx百科之gzip压缩、黑白名单、防盗链、零拷贝、跨域、双机热备

引言 早期的业务都是基于单体节点部署&#xff0c;由于前期访问流量不大&#xff0c;因此单体结构也可满足需求&#xff0c;但随着业务增长&#xff0c;流量也越来越大&#xff0c;那么最终单台服务器受到的访问压力也会逐步增高。时间一长&#xff0c;单台服务器性能无法跟上业…

【原创】H3C三层交换机VLAN路由

网络拓扑图 VLAN 配置 VLAN 100 VLAN 200 [H3C]int vlan 100 ip address 1.1.1.1 255.255.255.0[H3C-Vlan-interface100]int vlan 200 ip address 2.2.2.1 255.255.255.0[H3C]int GigabitEthernet 1/0/1port access vlan 100[H3C]int GigabitEthernet 1/0/2port access vlan 2…

使用Git和Github上传代码文件

1. 先检查是否安装好git git --version2. 输入你的github用户名 git config --global user.name "用户名"3. 输入你的github邮件 git config --global user.email "邮件地址"4. 设定git推送本地仓库中与远程仓库中具有相同名称的所有分支。 git config…

DevOps理念:开发与运维的融合

在现代软件开发领域&#xff0c;DevOps 不仅仅是一个流行的词汇&#xff0c;更是一种文化、一种哲学和一种方法论。DevOps 的核心理念是通过开发和运维之间的紧密合作&#xff0c;实现快速交付、高质量和持续创新。本文将深入探讨 DevOps 文化的重要性、原则以及如何在团队中实…

mysql异常占用资源排查

通过执行日志与连接信息排查 查看是否开启日志记录 mysql> show global variables like %general%; --------------------------------- | Variable_name | Value | --------------------------------- | general_log | OFF | | general_log_file…

Git——Windows平台创建gitee私有仓库详解

目录 1. 安装git 2. gitbash配置 2.1 设置 2.2 生成key 2.3 项目管理 2.3.1 本地新建 2.3.2 clone远程仓库的工程到本地改文件 1. 安装git 默认安装。 2. gitbash配置 2.1 设置 打开gitbash&#xff0c;设置用户名和邮箱&#xff1a; git config --global user.name …

Citespace、vosviewer、R语言的文献计量学 、SCI

文献计量学是指用数学和统计学的方法&#xff0c;定量地分析一切知识载体的交叉科学。它是集数学、统计学、文献学为一体&#xff0c;注重量化的综合性知识体系。特别是&#xff0c;信息可视化技术手段和方法的运用&#xff0c;可直观的展示主题的研究发展历程、研究现状、研究…