Linux网络编程(六)-TCP协议服务端及代码实现

news/2024/10/22 12:44:52/

1.概述

在讲述了那么多以后我们终于来到了代码阶段的讲解了,先放一张流程图便于大家理解。接着会为大家讲述具体的实现过程。

通过上图我们可以看到一个完整的Socket网络通信,是有客户端和服务端两部分代码组成的,即两个程序(你发给我,我接收;我发给你,你接收)组成。左侧为客户端,右侧为服务端。每一步都是由Socket为我们封装好的函数实现,简单说,我们只需要弄明白每一步的作用和使用方法即可。本章我们先着重讲解服务端的每一步,TCP服务端调用的函数依次是**socket( )、bind( )、listen( )、accept( )、recv( )、send( )、closesocket( ),**我们会逐一详细的介绍每一个函数的作用及用法。除此之外,我们还会为大家介绍一下服务端和客户端都会用到的WSAStartup( )函数,在windows系统中我们需要这个函数来以指明 WinSock 规范的版本。以及WSACleanup函数,这个函数用来终止对Socket字库的使用。

2.WSAStartup函数

上一节我们介绍了客户端与服务端相互通信的模型图,每一步都有具体的函数实现,但需要明白的是,使用这些函数之前,在Windows系统下,需要先调用WSAStartup函数进行必要的初始化,才可以顺利的进行,

而使用WSAStartup函数又需要事先先包含对应的头文件winsock2.h及静态库ws2_32.lib文件(在windows环境下)

具体如下:

函数功能:

用于初始化Socket编程,指明Windows系统中Socket( )版本

依赖静态库库:

ws2_32.lib

需要用#pragma命令包含,即:

#pragma comment(lib,"ws2_32.lib")    //表示链接Ws2_32.lib这个库。ws2_32.lib是Winsock2的库文件。

函数原型:

int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);

返回值类型:

整型

返回值:

成功返回0,失败返回-1

参数说明:

第一个参数wVersionRequested为 WinSock 规范的版本号它的类型WORD,所以要用MAKEWORD( )宏函数对它的版本号进行转换。低字节为主版本号,高字节为副版本号。

代码示例:

wVersionRequired=MAKEWORD(1,2)   //即主版本号是1,副版本号位2,那么它表示的就是调用WinSock 1.2版本。     现在我们一般用2.2版本 ,即MAKEWORD(1,2)

第二个参数lpWSAData 为指向 WSAData 结构体的指针。其定义原型如下:

typedef struct WSAData {WORD        wVersion;    //ws2_32.dll 建议我们使用的版本号WORD        wHighVersion;    //ws2_32.dll 支持的最高版本号
#ifdef _WIN64unsigned short iMaxSockets;     //2.0以后不再使用unsigned short iMaxUdpDg;     //2.0以后不再使用char        *lpVendorInfo;     //2.0以后不再使用char        szDescription[WSADESCRIPTION_LEN+1];  //一个以 null 结尾的字符串,用来说明 ws2_32.dll 的实现以及厂商信息char        szSystemStatus[WSASYS_STATUS_LEN+1];  //一个以 null 结尾的字符串,用来说明 ws2_32.dll 的状态以及配置信息
#elsechar        szDescription[WSADESCRIPTION_LEN+1];   //32位版本,同上char        szSystemStatus[WSASYS_STATUS_LEN+1];   //32位版本,同上unsigned short iMaxSockets;    //32位版本,同上unsigned short iMaxUdpDg;    //32位版本,同上char        *lpVendorInfo;    32位版本,同上
#endif
} WSADATA, *LPWSADATA;

怎么样,看清楚真实面貌了吧,因此我们只需要包含头文件、静态库,然后完善这两个参数,传入WSAStartup调用即可。

当然,完整可编译的代码,则需要将以上代码放入main函数中,并且包含对应的头文件及相对应的静态库,使用方式如下:

//www.dotcpp.com
#include <stdio.h>
#include <winsock2.h>
#pragma comment (lib, "ws2_32.lib")int main()
{
WSADATA wsaData;
WSAStartup( MAKEWORD(2, 2), &wsaData); //目前建议使用最新2.2版本//以下为测试信息,打印相应的数值用于测试
printf("wVersion: %d.%d\\n", LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion));
printf("wHighVersion: %d.%d\\n", LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
printf("szDescription: %s\\n", wsaData.szDescription);
printf("szSystemStatus: %s\\n", wsaData.szSystemStatus);return 0;
}

大家可以实行上机试验。

在介绍完WSAStartup函数后我们就要按顺序讲解socket( )、bind( )、listen( )、accept( )、recv( )、send( )、closesocket( )这些函数了。

 3.Socket函数

如下图所示,是Socket通信的原理图,左侧为服务端,右侧是客户端,可以看到服务端的步骤要多一些,客户端将在后面讲解。本节开始将从左侧服务端第一步开始逐步讲解,本步骤目标为创建一个套接字,其返回值为后面的步骤使用。

 

下面我们开始正式进入Socket通信的第一步,这一步无论是客户端还是服务端都是需要的第一步,因此大家认真思考。

函数功能:

创建套接字

头文件:

#include <winsock2.h>

函数原型:

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

返回值类型:

整型

返回值:

成功返回非负值,表示套接字的文件描述符,失败返回-1,通常返回-1错误很可能是没有执行 WSAStartup初始化导致!

参数说明:

第一个参数af指明了协议族,通常用AF_INET、AF_INET6、AF_LOCAL等。AF表示地址族,选择 AF_INET 的目的就是使用 IPv4 进行通信。因为 IPv4 使用 32 位地址,相比 IPv6 的 128 位来说,计算更快,便于用于局域网通信。

第二个参数type是Socket类型,常用的Socket类型我们之前已经介绍过了分别是SOCK_STREAM和SOCK_DGRAM因为我们要写的是TCP Socket编程所以我们使用SOCK_STREAM。

第三个参数protocol表示传输协议一般取为0。因为一般情况下有了 domain和 type 两个参数就可以创建套接字了,操作系统会自动推演出协议类型,除非遇到这样的情况:有两种不同的协议支持同一种地址类型和数据传输类型。如果我们不指明使用哪种协议,操作系统是没办法自动推演的。

调用socket函数整体代码的实现:

**int** sockfd=socket(AF_INET,SOCK_STREAM,0);//建立套接字

显而易见的,那么UDP的写法则为:

sockfd=socket(AF_INET, SOCK_DGRAM,0);

 4.Bind函数

在完成第一步创建套接字,分配了一个Socket描述符后,服务端的第二步就是使用在这个描述符用Bind绑定

Bind()系统调用的主要用处:

  1. 服务器向系统注册它的众所周知的地址。面向连接和无连接的服务器在接受客户的请求之前都必须做这一步。
  2. 客户可为自己注册一个特定的地址,以便服务器可以用这个有效的地址送回响应。

函数功能:

将监听套接字绑定到本地地址和端口上。

头文件:

#include <winsock2.h>

函数原型:

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

返回值类型:

整型

返回值:

成功返回非负值,失败返回-1,最常见的错误一般是端口被占用。需要注意的是,在Linux系统中,1024以下的端口都需要root权限的程序才可以绑定

参数说明:

第一个参数sockfd为上一步创建socket时的返回值。

第二个参数addr 为 sockaddr 结构体变量的指针。该类型的定义原型如下:

struct sockaddr_in {short   sin_family;    //协议族,与前面Socket函数中提到的一样,我们这里使用AF_INETu_short sin_port;        //端口号,需要struct in_addr sin_addr;    //IP地址,需要使用网络序char    sin_zero[8];    //没有实际意义,只是为了跟SOCKADDR结构在内存中对齐
};

第三个参数addrlen为addr 变量的大小,可由 sizeof() 计算得出。

调用bind函数整体代码的参考代码

struct sockaddr_in serv_addr    //创建结构体变量
servaddr.sin_family=AF_INET;    //sin_family指代协议族和前面讲述socket()的第一个参数的含义相同,取值也需跟socke函数第一个参数值一样。
servaddr.sin_port=htons(2000);    //sin_port存储端口号(使用网络字节顺序,对于htons()函数我们还有单独一章的说明,2000这个端口转换为网络字节序列。
//理论上端口号的取值范围为是0到65536,但0到1023的端口一般由系统分配给特定的服务程序,比如Web 服务的端口号为 80所以我们的程序要尽量在 1024~65536 之间分配端口号。servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");    //将iP地址127.0.0.1也就是本机地址转换为十进制bind(sockfd,(sockaddr*)&servaddr,sizeof(servaddr));    // 将套接字绑定到本地地址和端口上。

 5.listen函数

接着,在完成bind函数之后,服务端接下来就可以用listen函数监听了,用于监听是否有客户端连接它,以便存储多个用户的连接建立请求,listen函数具体如下:

函数功能:

让socket进入被动监听状态。什么是被动监听呢,是指当没有客户端请求时,socket处于“沉睡”中,只有当接收到客户端请求时,socket才会被“叫醒”来响应请求。

头文件:

#include <winsock2.h>

函数原型:

int listen(int sockfd, intqueue_length);

返回值类型:

整型

返回值:

成功返回0,失败返回-1

参数说明:

第一个参数为第一步sockfd创建socket时的返回值,套接字的描述符。

第二个参数queue_length用于指定接收队列的长度,也就是在Server程序调用accept函数之前最大允许进入的连接请求数,多余的连接请求将被拒绝,典型取值为5。

listen(sockfd,5);//监听sockfd为创建套接字时的返回值。

 6.accept函数

在listen监听到有新客户端时,就可以用accept函数响应客户的连接请求,建立与客户端的连接。产生一个新的socket描述符来描述该连接,这个连接用来与发起该连接请求的客户交换数据。

函数功能:

接收客户端连接请求

头文件:

#include <winsock2.h>

函数原型:

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

返回值类型:

整型

返回值:

成功返回非负值,失败返回-1

参数说明:

sockfd为建立socket函数返回的值。

addr为 sockaddr 结构体变量的指针,这个参数是指针类型,是向外传内容的,即addr将在函数调用后填入对方(客户端)的地址信息,如对方的IP、端口等。

addrlen为 addr变量的大小,可由 sizeof() 计算得出。

调用accept函数整体代码的实现:

struct sockaddr_in clientaddr//创建客户端地址结构体

int aID;//用来接收accept函数返回值

aID=accept(sockfd,(sockaddr*)&clientaddr,&sizeof( clientaddr));//等待接收客户连接请求

7.recv函数 

函数功能:

接收客户端或服务端传来的数据,也就是客户端和服务端都要用到

头文件:

#include <winsock2.h>

函数原型:

int recv(int aID, char *buf, int len, int flags);

返回值类型:

整型

返回值:

返回值小于0,socket报错。返回值等于0没有接收到数据,返回值大于0成功,返回值即为接收到的数据长度

参数说明:

第一个参数aID,表示连接成功的套接字描述符。

注意:这一步对于服务端而言是上一步accept的返回值;对于客户端而言是connect的返回值,并非是第一步socket创建套接字的返回值,请大家理解不要搞混!

第二个参数buf,就是为要接收的数据所在的缓冲区地址,也就是一个空的字符数组的首地址,这里放结果。

第三个参数len为要接收数据的字节数。

第四个参数flags为发送数据时的附带标记 ,一般情况下设置为0。但可以选择下列设置:

MSG_DONTROUTE:表示不使用指定路由,对send、sendto有效

MSG_PEEK:对recv, recvfrom有效,表示读出网络数据后不清除已读的数据

MSG_OOB:对发送接收都有效,表示发送或接受加急数据

调用recv函数整体代码的实现:

需要注意的是,recv缺省是阻塞函数,直到收到信息或出错才会返回。

char recBuf[200];//定义一个字符串用来保存客户端发来的数据
recv(aID,recBuf,200,0);//接收来自客户端或服务端的数据

 8.send函数

与是recv一样,有收就会有发,发送内容对应send函数,也是从服务端accept后或客户端connect后就可以用的函数,其说明如下:

函数功能:

发送服务端或客户端的数据

头文件:

#include <winsock2.h>

函数原型:

int send(int aID, const char *buf, int len, int flags);

返回值类型:

整型

返回值:

返回值小于0,socket报错。返回值等于0对方调用了close API来关闭连接,返回值大于0成功,返回值为发送的的数据长度

参数说明:

第一个参数aID,表示连接成功的套接字描述符。

注意:这一步对于服务端而言是上一步accept的返回值;对于客户端而言是connect的返回值,并非是第一步socket创建套接字的返回值,请大家理解不要搞混!

第二个参数buf为要发送的数据所在的缓冲区地址,即一个已经存好内容的字符数组

第三个参数len为要发送的数据的实际字节数+1。

第四个参数flags为发送数据时的附带标记 ,一般情况下设置为0。但可以选择下列设置:

MSG_DONTROUTE:表示不使用指定路由,对send、sendto有效

MSG_PEEK:对recv, recvfrom有效,表示读出网络数据后不清除已读的数据

MSG_OOB:对发送接收都有效,表示发送或接受加急数据

调用send函数整体代码的实现:

char sendBuf[200];//定义一个数组用来保存发送的数据
send(aID,sendBuf,strlen(sendBuf)+1,0);//用来发送服务端或客户端的数据

与recv同样,send函数缺省也是阻塞函数,直到发送完毕或出错才会返回。

需要注意,如果函数返回值与参数len不相等,则剩余未发送的信息需要再次发送。

9.closesocket函数

 一旦决定要停止通信,就要关闭套接字,释放资源,则需要调用closesocket函数进行

其函数介绍如下:

函数功能:

与socket函数功能相反关闭套接字

头文件:

#include <winsock2.h>

函数原型:

int closesocket(int aID);

返回值类型:

整型

参数说明:

aID为接收客户端请求的返回值。

调用closesocket函数整体代码的实现:

closesocket(aID);

 10.WSACleanup函数

一旦程序结束需要停止Socket库的使用,需要调用WSACleanup函数,这一步和最开始的WSACleanup是对应的。

函数功能

用于终止对So:cket字库的使用。

库链接:

#pragma comment(lib,"ws2_32.lib")表示链接Ws2_32.lib这个库。 ws2_32.lib是Winsock2的库文件。

函数原型:

int PASCAL FAR WSACleanup (void)。

返回值类型:

整型

返回值:

成功返回0

参数说明:

无参数

因此调用WSACleanup函数也很简单,如下:

WSACleanup();

 11.Socket服务端完整参考代码

前面讲解了Socket通信中服务端的每一步功能作用及实现,而重点是多个步骤在一起时,上下文 函数之间的信息传递需要我们理解,如SOCKADDR_IN的结构体、各个SOCKET描述符等参数,以及在此基础之上改进得到的希望的实际效果(如需要不停的接受消息、发送消息)

下面我们将所有步骤串联在一起,提供一个可以连续接收客户端信息的服务端程序,完整代码供大家参考:

#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{WSADATA wsaData;WSAStartup( MAKEWORD(2, 2), &wsaData); //目前建议使用最新2.2版本SOCKET serSocket=socket(AF_INET,SOCK_STREAM,0);//创建了可识别套接字if(serSocket!=-1){printf("成功创建套接字!%d\\n",serSocket);  }//需要绑定的参数,主要是本地的socket的一些信息。SOCKADDR_IN addr;addr.sin_family=AF_INET;addr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);//ip地址addr.sin_port=htons(12345);//绑定端口bind(serSocket,(SOCKADDR*)&addr,sizeof(SOCKADDR));//绑定完成listen(serSocket,5);//其中第二个参数代表能够接收的最多的连接数printf("等待客户端...\\n");SOCKADDR_IN clientsocket;int len=sizeof(SOCKADDR);//第二次握手,通过accept来接受对方的套接字的信息SOCKET serConn=accept(serSocket,(SOCKADDR*)&clientsocket,&len);//如果这里不是accept而是conection的话。。就会不断的监听if(serConn){printf("监听到新的客户端...\\n");}while (1){char sendBuf[100];sprintf(sendBuf,"welcome %s to here",inet_ntoa(clientsocket.sin_addr));//找对对应的IP并且将这行字打印到那里//发送信息send(serConn,sendBuf,strlen(sendBuf)+1,0);char receiveBuf[100];//接收int RecvLen;RecvLen=recv(serConn,receiveBuf,100,0);if(RecvLen!=-1)printf("%d %s\\n",RecvLen,receiveBuf);elsebreak;}closesocket(serConn);//关闭WSACleanup();//释放资源的操作return 0;
}

因为暂时没有客户端连接,运行后为等待效果状态,效果如下:


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

相关文章

Android TextView实现一串文字特定几个字改变颜色

遇到一个需求&#xff0c;让Android端实现给定一个字符串指定下标的几个字颜色与其他字颜色不一致。 主要是用ForegroundColorSpan这个API来传入颜色值&#xff0c;用SpannableString来设置指定索引下标的字的颜色值。 这里通过给定一个输入文字描述框&#xff0c;要求输入指定…

Mysql 和MongoDB用户访问权限问题

Mysql 刚给二线运维排查了一个问题&#xff0c;Mysql安装完可用&#xff0c;且可用navicat连接&#xff0c;项目中通过127.0.0.1去连数据库报错了。错误是access denied for user ‘root’localhost,排查思路 1. 密码是否正确 &#xff08;不需要重置。到Mysql的安装目录下找…

流批一体计算引擎-17-[Flink]中的Table API常用算子

文章目录 1 概述&示例1.1 data.csv1.2 代码示例2 操作算子2.1 扫描、投影和过滤2.1.1 from_path【流批】2.1.2 from_elements【流批】2.1.3 select【流批】2.1.4 alias【流批】2.1.5 where【流批】2.1.6 filter【流批】2.2 列操作2.2.1 add_columns【流批】2.2.2 add_or_re…

13 django管理系统 - 注册与登录 - 中间件控制访问

去管理员列表中&#xff0c;获取刚才登录的用户session&#xff1a; 用户发来请求&#xff0c;获取cookie随机字符串&#xff0c;拿着随机字符串&#xff0c;看看session中有没有 通过request.session.get("user_info")来获取 def admin_list(request):# 获取当前登…

安装和简单使用Milvus

安装和简单使用Milvus 1 介绍 Milvus是国产的高性能分布式向量数据库。 # Milvus官网 https://milvus.io/# 安装文档 https://milvus.io/docs/install-overview.md# Python的对应关系和接口文档 https://milvus.io/api-reference/pymilvus/v2.4.x/About.md2 安装Milvus 2.1…

解析带有MyBatis语法的SQL字符串,获取最终的可执行SQL

有一个带有mybatis语法的sql语句&#xff0c;如下&#xff1a; select * from cfg_export_template where id #{id} <if testid ! null> AND 1 1 </if>需求是将sql和占位符相关的数据丢给mybatis&#xff0c;获取到最终可执行的SQL。 解决思路&#xff1a; 1、像…

R语言绘制Venn图(文氏图、温氏图、维恩图、范氏图、韦恩图)

Venn图&#xff0c;又称文氏图&#xff0c;标题中其他名字也是它的别称&#xff0c;由封闭圆形组成&#xff0c;代表不同集合。圆形重叠部分表示集合交集&#xff0c;非重叠处为独有元素。在生物学、统计学等领域广泛应用&#xff0c;可展示不同数据集相似性与差异&#xff0c;…

企业电子印章主要通过以下几种方式进行防伪

企业电子印章主要通过以下几种方式进行防伪&#xff1a; 一、数字证书和加密技术 数字证书认证 企业电子印章依托数字证书&#xff0c;数字证书由权威的第三方数字认证机构颁发&#xff0c;确保了印章使用者的身份真实性。 数字证书如同企业在数字世界的身份证&#xff0c;包…