600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > LinuxC++:网络编程(一)最原始服务端及客户端代码实现和函数释义

LinuxC++:网络编程(一)最原始服务端及客户端代码实现和函数释义

时间:2023-05-24 07:36:38

相关推荐

LinuxC++:网络编程(一)最原始服务端及客户端代码实现和函数释义

前言:学习编程一定要敲,接着测试,然后查资料,最后总结!!!

一、socket这个单词在C++网络编程中的含义

socket中文就是插座。运行在计算机中的两个程序通过socket建立起一个通道,数据在通道中传输。socket把复杂的TCP/IP协议族隐藏了起来,对程序员来说,只要用好socket相关的函数,就可以完成网络通信。

二、socket分类

socket有两种通信机制,如下:

:在实际开发中,数据报socket的应用场景极少,本文也只介绍流socket。

客户端和服务端流程简介

三、服务端代码实现

这部分是我自己改编的和参考资料的略有不同,但逻辑是相同的。

#include <stdio.h>#include <sys/socket.h>#include <arpa/inet.h>#include <string.h>#include <stdlib.h>#include <unistd.h>#include <string>#include <iostream>using namespace std;int main(int argc, char *argv[]){if (argc != 2){printf("Warning:Parameters must be set!\nPls Using:./server portNumber\nExample:./server 5005\n\n");return -1;}printf("-------------------Server start!\n");// 第1步:创建服务端的socket。int serverSocket;serverSocket = socket(AF_INET, SOCK_STREAM, 0); //AF_INET 协议族,SOCK_STREAM TCPsocket类型:失败返回-1,成功返回该socket的整型数值//#include <sys/socket.h>if (serverSocket == -1){perror("create socket error:\n"); //perror(s) 就是将发生错误的函数的message(也就是errno中的message)和s的信息一起打印出来。此处就是socket+socket失败后存在全局变量errno中的messagereturn -1;}else{printf("1.Create Socket Successfully\n");}//第2步:把服务端用于通信的地址和端口绑定到socket上。//2.1 给地址信息结构体赋上传入的值//新建结构体指针 并进行内存清0操作struct sockaddr_in servaddr;// 地址信息的数据结构。//#include <arpa/inet.h>memset(&servaddr, 0, sizeof(servaddr)); //将第一个参数后的第三个参数大小的内存置为第二个参数的直,就是内存清0常用函数//#include <string.h>//协议族赋值servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。//IP地址赋值 可指定IP或获取本机任意IP//比如一个主机三个网卡,要接收所有网卡收取的信息就要绑定三次,但是可以通过I NADDR_ANY 可以直接接收所有的网卡收到的信息servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本机任意ip地址都能收到。 htonl 将一个32位数从主机字节顺序转换成网络字节顺序。//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); //指定ip地址。//端口号赋值servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。atoi把字符串转换成整型数的一个函数,会跳过前面的空白字符(例如空格,tab缩进)等//#include <stdlib.h>//将通信信息绑定到创建的服务端socket上int bindResult;bindResult = bind(serverSocket, (struct sockaddr *)&servaddr, sizeof(servaddr)); //成功返回0,失败返回-1,错误存在errno中//获取指针长度用*servaddrif (bindResult == -1){perror("bind error:\n");close(serverSocket); //即使失败也要关闭socket,释放资源 //#include <unistd.h>return -1;}else{printf("2.Bind successfully!\n");}// 第3步:把socket设置为监听模式。服务端特有的int listenResult;listenResult = listen(serverSocket, 5); //0-成功, -1-失败;第二个参数一般不超过30if (listenResult == -1){perror("listen error:\n");close(serverSocket);return -1;}else{printf("3.Listen successfully!\n");}// 第4步:接受客户端的连接。//新建一个用于存储客户端通信信息的结构体指针struct sockaddr_in clientaddr;int socklen = sizeof(struct sockaddr_in); int acceptResult;acceptResult = accept(serverSocket, (struct sockaddr *)&clientaddr, (socklen_t *)&socklen); //如果客户端没连上会一直处于等待状态,又称阻塞; 第三个参数和bind的第三个参数不同,是用来存放客户端通信信息的if (acceptResult == -1){printf("Server accept error:\n");close(serverSocket);return -1;}else{printf("4.Client Accept! ip:(%s)\n ---------------Communication Start---------\nWarning:If you input 'bye',this communication will end!!!\n", inet_ntoa(clientaddr.sin_addr));}char Getmessage[1024];//变量名就代表该数组首地址while (1){int recvResult;memset(Getmessage,0,sizeof(Getmessage));//内存清零操作//recv函数对端关闭返回0,出错返回-1recvResult=recv(acceptResult,Getmessage,sizeof(Getmessage),0);// 第二个参数用来接收链接上的客户端发送的消息,第三个参数要接收信息的长度,不能超过接收实际信息的长度;第二个参数虽然是指针类型,但数组名等同于char * message;的变量名的意义if (recvResult<=0){printf("recv error!\n");break;}else{printf("@@GetMessage:\n%s\n",Getmessage);if (((string)Getmessage)=="bye"){printf("---------------Communication End---------\n");break;}else{printf("##PlsInputMessage:\n");string sendMessage;cin>>sendMessage;int sendResult;与recv函数一样对端关闭返回0,出错返回-1sendResult=send(acceptResult,(const char *)(sendMessage.data()),sizeof(sendMessage),0);if (sendResult<=0){perror("send error:\n");break;}else{printf("---Send Successfully! Waiting the other party send a message...\n");if ((string)sendMessage=="bye"){printf("---------------Communication End---------\n");break;}}}}}close(serverSocket);close(acceptResult);}

四、客户端代码实现

#include <stdio.h>#include <sys/socket.h>#include <netdb.h>#include <unistd.h>#include <string.h>#include <stdlib.h>#include <arpa/inet.h>#include <string>#include <iostream>using namespace std;int main(int argc, char *argv[]){if (argc != 3) //运行客户端需要知道IP地址及端口{printf("Warning:Parameter must be set\n Using:./client ip port\nExample:./client 127.0.0.1 5005\n\n");return -1;}// 第1步:创建客户端的socket。int clientSocket;clientSocket = socket(AF_INET, SOCK_STREAM, 0);if (clientSocket == -1) //socket函数返回值实际是一个文件描述符,是一个整数;失败返回-1{perror("socket erroe:\n");return -1;}else{printf("1.Create Socket Successfully!\n");}// 第2步:向服务器发起连接请求。struct hostent *h;h = gethostbyname(argv[1]); //gethostbyname将ip地址或域名转化为hostent结构体表达式的地址(指针地址);失败返回NULLif (h == NULL){printf("gethostbyname failed.\n");close(clientSocket);return -1;}struct sockaddr_in servaddr;memset(&servaddr, 0, sizeof(servaddr)); //memset:将&servaddr中当前位置后sizeof(servaddr)个字节用0代替。通常为新申请的内存做初始化工作,也是对较大的结构体或数组进行清零操作的一种最快方法。//第二个参数设置为0是为了将,&servaddr开始的地址内存按sizeof(servaddr)个字节清0servaddr.sin_family = AF_INET;servaddr.sin_port = htons(atoi(argv[2])); // 指定服务端的通信端口。memcpy(&servaddr.sin_addr, h->h_addr, h->h_length); //memcpy:内存拷贝函数。将h->h_addr的h->h_length个字符拷贝到&servaddr.sin_addr中。int connectResult;connectResult = connect(clientSocket, (struct sockaddr *)&servaddr, sizeof(servaddr)); //将创建的客户端socket连接至服务端,第二个参数为(struct sockaddr *)类型,第三个为第二个参数的大小;成功返回0,失败返回-1if (connectResult == -1){perror("connect error");close(clientSocket);return -1;}else{printf("2.Connect Successfully! ip:(%s)\n ---------------Communication Start---------------\n Warning:If you input 'bye',this communication will end!!!\n", inet_ntoa(servaddr.sin_addr));}// char buffer[1024]; //开辟1Kb空间;字符数组,可在栈或堆上分配空间,堆上需手动管理内存。// 第3步:与服务端通信,发送一个报文后等待回复,然后再发下一个报文。while (1){int sendResult;string sendMessage;//使用string时无法输入空格,因为容器string遇到空格会截断数据printf("##PlsInputMessage:\n");cin >> sendMessage;sendResult = send(clientSocket, (const char *)sendMessage.data(), sizeof(sendMessage), 0);if (sendResult <= 0){perror("send error:\n");break;}else{if (sendMessage=="bye"){printf("---------------Communication End---------------\n");break;}else{printf("---Send Successfully! Waiting the other party send a message...\n");string Getmessage;int recvResult;recvResult = recv(clientSocket, (char *)(Getmessage.c_str()), sizeof(Getmessage), 0);if (recvResult <= 0){printf("recv error!\n");break;}else{printf("@@GetMessage:\n%s\n", (char *)Getmessage.c_str());if ((string)Getmessage.c_str()=="bye"){printf("---------------Communication End---------------\n");break;}}}}}}

五、测试的客户端和服务端程序

自行将上面的代码编译后测试,这里不做赘述。

注意:记得设置端口时,要提前将端口的防火墙打开(此命令参照连接)

服务端函数详解

服务端函数调用的流程是:socket->bind->listen->accept->recv/send->close

1. socket函数

预备知识:

在UNIX系统中,一切输入输出设备皆文件,socket()函数的返回值其本质是一个文件描述符,是一个无符号整数。

unix系统中默认已有三个文件描述符如下:

0:标准输入1:标准输出2:标准错误

故socket程序中使用socket创建的描述符返回值一般是从3开始返回。

但GDB调试时是从7开始的。

- socket函数作用:socket函数用于创建一个新的socket,也就是向系统申请一个socket资源。

- 函数声明int socket(int domain, int type, int protocol);

- 返回值:成功则返回一个socket,失败返回-1,错误原因存于errno 中

- 参数说明

第一个参数(domain)只能填AF_INET;

第二个参数(type)只能填SOCK_STREAM;这个参数能填好多种可自行百度了解。

第三个参数(protocol)只能填0。

domain:协议域,又称协议族(family)。常用的协议族有AF_INET、AF_INET6、AF_LOCAL(或称AF_UNIX,Unix域Socket)、AF_ROUTE等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。

type:指定socket类型。常用的socket类型有SOCK_STREAM、SOCK_DGRAM、SOCK_RAW、SOCK_PACKET、SOCK_SEQPACKET等。流式socket(SOCK_STREAM)是一种面向连接的socket,针对于面向连接的TCP服务应用。数据报式socket(SOCK_DGRAM)是一种无连接的socket,对应于无连接的UDP服务应用。

protocol:指定协议。常用协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_STCP、IPPROTO_TIPC等,分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

关于此函数可用如下命令在linux系统中查看原文函数释义:

man socket

2. bind函数

- 作用:服务端把用于通信的地址和端口绑定到socket上。

- 函数声明int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。

如果绑定的地址错误,或端口已被占用,bind函数一定会报错,否则一般不会返回错误。

- 参数说明

第一个参数(sockfd)为要绑定的socket对于应的文件描述符的值,也就是socket函数返回的整数;

第二个参数(addr)为结构体sockaddr,一般我们会先定义结构体sockaddr_in(struct sockaddr_in servaddr;)并给其结构体中的成员赋值,然后再强转为sockaddr* 类型((struct sockaddr *)&servaddr)。

第三个参数(addrlen)表示第二个参数addr结构体的大小。一般直接sizeof(servaddr)即可;

3. listen函数

- 作用:listen函数把主动连接socket变为被动连接的socket,使得这个socket可以接受其它socket的连接请求,从而成为一个服务端的socket。

- 函数声明int listen(int sockfd, int backlog);

- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。

- 参数说明

第一个参数(sockfd):为已经被bind过的socket描述符;

socket函数返回的socket是一个主动连接的socket。在服务端的编程中,程序员希望这个socket可以接受外来的连接请求,也就是被动等待客户端来连接。由于系统默认时认为一个socket是主动连接的,所以需要通过某种方式来告诉系统,程序员通过调用listen函数来完成这件事。

第二个参数(backlog):为整型数字;是监听到客户端连接完成三次握手队列的个数;这个参数涉及到一些TCP的细节,参照本文章后面补充知识的TCP模块,这里填5、10都行,一般不超过30。

注:当调用listen之后的socket就可以再使用accept来接受客户端的连接请求。

4. accept函数

- 作用:服务端接收客户端的连接。如果listen监听到有客户端链接上来,那么会将该客户端加到监听的队列里,accept会从监听的队列里获取一个请求。如果监听到客户端连接的队列为空,那么accept阻塞等待。

- 函数声明int accept(int sockfd,struct sockaddr *addr,socklen_t *addrlen);

- 返回值:成功则返回0,失败返回-1,错误原因存于errno 中。

accept在等待的过程中,如果被中断或其它的原因,函数返回-1,表示失败,如果失败,可以重新accept。

- 参数说明

第一个参数(sockfd):是已经被listen函数调用过的socket文件描述符。

第二个参数(addr):同bind函数的第二个参数;用于存放接收监听到链接上来的客户端的地址信息。如果不需要客户端的地址,可以填0(这个参数是个地址,0也就是NULL)。

第三个参数(addrlen):用于存放第二个参数的长度,如果addr为0,addrlen也填0。

4. recv函数

- 作用:用于接收对端socket发送过来的数据。不论是客户端还是服务端,应用程序都用recv函数接收来自TCP连接的另一端发送过来数据。

- 函数声明ssize_t recv(int sockfd, void *buf, size_t len, int flags);

ssize_t 就是 长整型long int。

- 返回值

1.如果socket的对端没有发送数据,recv函数就会等待

2.如果对端发送了数据,函数返回接收到的字符数

3.对端关闭,返回值为0

4.出错时返回-1

- 参数说明

第一个参数(sockfd):为accept函数返回值。

第二个参数(buf):为用于接收数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,只要是一块内存就行了。

第三个参数(len):len需要接收数据的长度,不能超过第二个参数(buf)的大小,否则内存溢出。

第四个参数(flags):一般填0, 其他数值意义不大。

5. send函数

- 作用:把数据通过socket发送给对端。不论是客户端还是服务端,应用程序都用send函数来向TCP连接的另一端发送数据。

- 函数声明ssize_t send(int sockfd, const void *buf, size_t len, int flags);

ssize_t 就是 长整型long int。

- 返回值

函数返回已发送的字符数,出错时返回-1,错误信息errno被标记。

就算是网络断开,或socket已被对端关闭,send函数不会立即报错,要过几秒才会报错。

- 参数说明

第一个参数(sockfd):服务端为accept函数返回的socket;客户端为最初创建的socket。

第二个参数(buf):为需要发送的数据的内存地址,可以是C语言基本数据类型变量的地址,也可以数组、结构体、字符串,内存中有什么就发送什么。

第三个参数(len):len需要接收数据的长度,不能超过第二个参数(buf)的大小,否则内存溢出。

第四个参数(flags):一般填0, 其他数值意义不大。

总结

服务端总共用到两个文件描述符(socket):

1.用于建立通道并绑定地址端口(bind)和用来监听(listen)以及等待(accept)的最初创建的socket。

2.由accept函数创建的用于与客户端收发报文的socket。

如果send函数和recv返回值(<=0),表示通信链路已不可用。

5. close函数

- 作用:关闭socket

- 函数声明int close(int sockfd);

- 返回值: 成功返回0,出错为-1

- 参数说明:只有一个参数sockfd,就是文件描述符(socket)的值。

六、客户端函数详解

客户端函数调用的流程是:socket->connect->send/recv->close

1. socket函数

同服务端。

2. gethostbyname函数

- 作用:把ip地址或域名转换为hostent 结构体表达的地址。它还有解析域名的作用。

- 函数声明struct hostent *gethostbyname(const char *name);

- 返回值:如果成功,返回一个hostent结构指针,失败返回NULL。

- 参数说明:name:域名或者主机名,例如"192.168.1.3"、""等。

3. connect函数

- 作用:向服务器发起连接请求。只用于客户端。connect函数用于将参数sockfd 的socket 连至参数serv_addr 指定的服务端,参数addrlen为sockaddr的结构长度。

- 函数声明int connect(int sockfd, struct sockaddr * serv_addr, int addrlen);

- 返回值: 成功则返回0,失败返回-1,错误原因存于errno 中。如果服务端的地址错了,或端口错了,或服务端没有启动,connect一定会失败。

- 参数说明

第一个参数(sockfd)为要绑定的socket对于应的文件描述符的值,也就是socket函数返回的无符号整数;

第二个参数(addr)为结构体sockaddr,一般我们会先定义结构体sockaddr_in(struct sockaddr_in servaddr;)并给其结构体中的成员赋值,然后再强转为sockaddr* 类型((struct sockaddr *)&servaddr)。

第三个参数(addrlen)表示第二个参数addr结构体的大小。一般直接sizeof(servaddr)即可;

4. recv函数

同服务端

5. send函数

通服务端

6. close 函数

同服务端

七、关闭socket

socket是系统资源,操作系统打开的socket数量是有限的,在程序退出之前必须关闭已打开的socket,就像关闭文件指针一样,就像delete已分配的内存一样,极其重要。

八、调试时可能遇到的问题

1. adress already in use

情况1:如果一个服务程序(系统中运行的任一程序)已经使用了端口5005,如果你再启动一个服务端程序绑定这个端口,那bind就会报这个错。

纠错:这个自己来吧。

情况2:kill掉一个已经绑定了5005的端口后,再次运行服务程序并绑定这个端口,也可能会报这个错。

纠错:这个涉及LINUX的TIME_WAIT,一般是两分钟;解决办法就是在listen函数调用后再给这个socket增加如下代码,让端口释放后可以立即被使用:

int opt=1; unsigned int len=sizeof(opt); setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&opt,len);

九、补充知识

- 在设置服务端地址时应注意下面这种情况:

服务器可能有多个网卡,如下图示例:

网卡1:地址:192.168.10.10;192.168.10.12和192.168.10.13访问服务器时只能通过该网卡;10.153.10.20和10.153.10.22就不能通过该网卡访问服务器。

网卡2地址:10.153.10.10;10.153.10.20和10.153.10.22访问服务器时只能通过该网卡;192.168.10.12和192.168.10.13就不能通过该网卡访问服务器

- bind和connect函数的第二个参数sockaddr_in

示例代码如下:

设置地址时需将服务端bind和connect的servaddr.sin_addr.s_addr结构设置一样,比如都采用主机字节序,或都不采用主机字节序。否则会产生错误。一般开发都使用网络字节序。

struct sockaddr_in servaddr;// 地址信息的数据结构。//#include <arpa/inet.h>memset(&servaddr, 0, sizeof(servaddr)); //将第一个参数后的第三个参数大小的内存置为第二个参数的直,就是内存清0常用函数//#include <string.h>//协议族赋值servaddr.sin_family = AF_INET; // 协议族,在socket编程中只能是AF_INET。//IP地址赋值 可指定IP或获取本机任意IP//比如一个主机三个网卡,要接收所有网卡收取的信息就要绑定三次,但是可以通过I NADDR_ANY 可以直接接收所有的网卡收到的信息servaddr.sin_addr.s_addr = htonl(INADDR_ANY); //本机任意ip地址都能收到。 htonl 将一个32位数从主机字节顺序转换成网络字节顺序。//servaddr.sin_addr.s_addr = inet_addr("192.168.190.134"); //指定ip地址。//端口号赋值servaddr.sin_port = htons(atoi(argv[1])); // 指定通信端口。atoi把字符串转换成整型数的一个函数,会跳过前面的空白字符(例如空格,tab缩进)等//#include <stdlib.h>

- bind端口绑定

如果一个服务程序(系统中运行的任一程序)已经使用了端口5005,如果你再启动一个服务端程序绑定这个端口,那bind就会报错。

端口范围(结构体sockaddr_in中端口号为short型,最大65535):1024~65535

- 打开文件描述符个数问题

//查看能打开的文件操作符个数;一般默认为1024。也就是socket最多能打开1024个ulimit -n

- 主机字节序与网络字节序

TCP报文中必须使用网络字节序,也就是大端字节序。recv函数和send函数在发送接收数据时都要遵循此规则。

什么是字节序:

网络字节序(大端字节序)和主机字节序(不确定):

下面是将网络字节序(默认大端)转为小端的转换示例:

转换为二进制后再将二进制按8位进行顺序颠倒后,即得小端字节序的二进制码。

涉及的字节序转换问题(参考链接):

hotns()——"Host to NetWork Short",主机字节顺序转换为网络字节顺序(对无符号短型进行操作 4bytes)htonl()——"Host to NetWork Long",主机字节顺序转换为网络字节顺序(对无符号长型进行操作 8bytes)ntons()——"NetWork to Host short",网络字节序转换为主机字节顺序(对无符号短型进行操作 4bytes)ntohl()——"NetWork to Host Long",网络字节顺序转换为主机字节顺序(对无符号长型进行操作 8bytes)

- 结构体sockaddr及sockaddr_in

服务端 结构体这块总共涉及三个:sockaddr、sockaddr_in、in_addr.

参考链接

客户端结构体:

- 监听队列等问题

TCP三次握手是在底层实现的,这部分了解即可。

解析:下图中客户端在左,服务端在右。客户端SYN_SENT状态和服务端的SYN_RECV状态简单理解为半连接(三次握手未成功,正在进行中);客户端ESTABLISHED状态和服务端的ESTABLISHED状态简单理解为全连接(也就是三次握手成功状态);客户端注意一个问题就是客户端响应了服务端半连接状态返回的额请求会直接变为握手成功ESTABLISHED状态,如果返回给服务端过程中失败,那此次握手应该是失败的,但客户端查看时还会是成功状态(ESTABLISHED)。服务端会有一个队列(SYN_RECV队列),用来存放客户端socket发送过来的连接请求,这个队列里服务端的TCP状态为SYN_RECV;服务端还会有一个队列(ESTABLISHED队列),用来存放和客户端三次握手成功的请求,这个队列里服务端的TCP状态为ESTABLISHED。也就是说服务端accept函数阻塞等待的是ESTABLISHED队列中的客户端请求。listen函数第二个参数的大小就是指listen监听到的客户端连接完成三次握手队列的个数;也就是上面提到的服务端队列(ESTABLISHED队列)的大小。服务端半连接状态队列大小由系统默认配置,可通过下面代码查看:cat /proc/sys/net/ipv4/tcp_max_syn_backlog//LINUX查看TCP三次握手状态的方法如下netstat na|more

- 服务端接收到的客户端的地址问题

如果服务端和客户端不在一个局域网内,那么两者通信时,服务端接收到的客户端的地址就是客户端所在的公网IP地址。

通过ifconfig命令查看到的inet地址只是局域网地址。

- send和recv缓存区问题

send函数和recv函数都有一个缓存区的问题。send的数据是先放到缓存区内,然后再发送出去;recv函数也是有缓存区,先接收到缓存区,然后再接收过来。可以把下图的两个socket的地方理解为两个缓存区。

send的缓存问题是可以测试出来的:比如一对建立好通信的服务端和客户端。客户端send发的快,而服务端recv接收慢,就可以看到send到后面会多行多行的写出来,这是因为send的缓存区发送出去一批就会有一批再到缓存区里;这就是缓存区的问题导致的。

- TCP报文分包和黏包问题

这个自定义协议在后面的文章中做补充

备注:

参考资料链接

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。