前言:学习编程一定要敲,接着测试,然后查资料,最后总结!!!
socket这个单词在C++网络编程中的含义
socket中文就是插座。运行在计算机中的两个程序通过socket建立起一个通道,数据在通道中传输。
socket把复杂的TCP/IP协议族隐藏了起来,对程序员来说,只要用好socket相关的函数,就可以完成网络通信。
socket分类
socket有两种通信机制,如下:
流(stream) | 数据报(datagram) | |
---|---|---|
又称 | 流socket | 数据报socket |
依赖协议 | 基于TCP协议 | UDP协议 |
效果 | 是一个有序、可靠、双向字节流的通道,传输数据不会丢失、不会重复、顺序也不会错乱。就像两个人在打电话,接通后就在线了,您一句我一句的聊天。 | 不需要建立和维持连接,可能会丢失或错乱。UDP不是一个可靠的协议,对数据的长度有限制,但是它的速度比较高。就像短信功能,一个人向另一个人发短信,对方不一定能收到。 |
注:在实际开发中,数据报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中的message
return -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中//获取指针长度用*servaddr
if (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-失败;第二个参数一般不超过30
if (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,出错返回-1
recvResult=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,出错返回-1
sendResult=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结构体表达式的地址(指针地址);失败返回NULL
if (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)个字节清0
servaddr.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,失败返回-1
if (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):为整型数字;这个参数涉及到一些网络的细节,比较麻烦,填5、10都行,一般不超过30。
注:当调用listen之后的socket就可以再使用accept来接受客户端的连接请求。
4. 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"、"www.freecplus.net"等。
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:地址: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就会报错。
- 打开文件描述符个数问题
//查看能打开的文件操作符个数;一般默认为1024。也就是socket最多能打开1024个
ulimit -n
- 主机字节序与网络字节序
什么是字节序:
网络字节序(大端字节序)和主机字节序(不确定):
下面是将网络字节序(默认大端)转为小端的转换示例:
转换为二进制后再将二进制按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.
参考链接
客户端结构体:
备注:
参考资料链接