网络编程是C++ API操作中很重要的一部分,包含TCP和UDP。
网络传输模型可以抽象为7个层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
。
但在使用TCP/IP协议时,可以简化为这4层:网络接口、网络层、传输层、应用层
。
名词
TCP:可靠传输,三次握手建立连接,传出去一定接受的到(如聊天软件);
UDP:不可靠传输,不需要建立连接,只管发送,实时性好(如视频会议);
套接字:表示通信的端点。就像用电话通信,套接字相当于电话,IP地址相当于总机号码,而端口号则相当于分机号码。
TCP
服务端创建流程:
- 调用socket函数创建监听socket
- 调用bind函数将socket绑定到某个IP和端口号组成的二元组上
- 调用listen函数开启监听
- 当有客户端连接请求时,调用accept函数接受连接,产生一个新的socket(与客户端通信的socket)
- 基于新产生的socket调用send或recv函数开始与客户端进行数据交流
- 通信结束后,调用close函数关闭socket
客户端创建流程:
- 调用socket函数创建客户端socket
- 调用connect函数尝试连接服务器
- 连接成功后调用send或recv函数与服务器进行数据交流
- 通信结束后,调用close函数关闭监听socket
服务端代码:
#include <iostream>
#include <sys/types.h> //基本系统数据类型
#include <arpa/inet.h> //网络信息转换
#include <unistd.h> //POSIX系统API访问
#include <string.h>using namespace std;
int main() {// 创建一个监听socketint listenfd = socket(AF_INET, SOCK_STREAM, 0); //常见的AF_INET──指定为IPv4协议,AF_INET6──指定为IPv6,AF_LOCAL──指定为UNIX 协议域//套接口可能的类型有:SOCK_STREAM字节流、SOCK_DGRAM数据报、SOCK_SEQPACKET有序分组、SOCK_RAW原始套接口//传输协议TCP/UDP,这里默认0if (listenfd == -1) {cout << " create listen socket error " << endl;return -1;}// 初始化服务器地址struct sockaddr_in bindaddr;bindaddr.sin_family = AF_INET;bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);bindaddr.sin_port = htons(8081);//如果只想在本机上进行访问,bind函数地址可以使用本地回环地址//如果只想被局域网的内部机器访问,那么bind函数地址可以使用局域网地址//如果希望被公网访问,那么bind函数地址可以使用INADDR_ANY or 0.0.0.0if (bind(listenfd, (struct sockaddr *)& bindaddr, sizeof(bindaddr)) == -1) {cout << "bind listen socket error" << endl;return -1;}// 启动监听if (listen(listenfd, SOMAXCONN) == -1) {cout << "listen error" << endl;return -1;}cout << "开始监听" << endl;while (true) {// 创建一个临时的客户端socketstruct sockaddr_in clientaddr;socklen_t clientaddrlen = sizeof(clientaddr);// 接受客户端连接int clientfd = accept(listenfd, (struct sockaddr *)& clientaddr, &clientaddrlen);if (clientfd != -1) {char recvBuf[32] = {0};// 从客户端接受数据int ret = recv(clientfd, recvBuf, 32, 0);if (ret > 0) {cout << "recv data from cilent , data:" << recvBuf << endl;// 将接收到的数据原封不动地发给客户端ret = send(clientfd, recvBuf, strlen(recvBuf), 0);if (ret != strlen(recvBuf)) {cout << "send data error" << endl;} else {cout << "send data to client successfully, data " << recvBuf <<endl;}} else {cout << "recv data error" <<endl;}close(clientfd);}}// 关闭监听socketclose(listenfd);return 0;
}
客户端代码:
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 8081
#define SEND_DATA "helloworld"using namespace std;int main() {// 创建一个socketint clientfd = socket(AF_INET, SOCK_STREAM, 0);if (clientfd == -1) {cout << " create client socket error " << endl;return -1;}// 连接服务器struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);serveraddr.sin_port = htons(SERVER_PORT);if (connect(clientfd, (struct sockaddr *)& serveraddr, sizeof(serveraddr)) == -1) {cout << "connect socket error" << endl;return -1;}// 向服务器发送数据int ret = send(clientfd, SEND_DATA, strlen(SEND_DATA), 0);if (ret != strlen(SEND_DATA)) {cout << "send data error" << endl;return -1;} else {cout << "send data to client successfully, data " << SEND_DATA <<endl;}// 从服务器拉取数据char recvBuf[32] = {0};ret = recv(clientfd, recvBuf, 32, 0);if (ret > 0) {cout << "recv data to client successfully, data " << recvBuf <<endl;} else {cout << "recv data to client error" << endl;}// 关闭socketclose(clientfd);return 0;
}
效果如下:
UDP
接收端创建流程:
- 创建套接字
- 将套接字绑定到一个本地地址和端口上(bind)
- 等待接受数据(recv)
- 关闭套接字。
发送端创建流程:
- 创建套接字
- 向服务器发送数据(send)
- 关闭套接字
UDP通信时,不强调server和client,重在实现两者互通;接收端需要bind,而发送端不需要。bind的一方只有接收到消息后才能开始发送。
设置两个端口实现互通:
接收端:
#include <sys/select.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstdlib> //标准库头文件
#include <cstdio> //c标准库
#include <cstring> //c字符操作
#include <iostream>int main(){//同一台电脑测试,需要两个端口int port_in = 12321;int port_out = 12322;int sockfd;// 创建socketsockfd = socket(AF_INET, SOCK_DGRAM, 0);if(-1==sockfd){return false;puts("Failed to create socket");}// 设置地址与端口struct sockaddr_in addr;socklen_t addr_len=sizeof(addr);memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET; // Use IPV4addr.sin_port = htons(port_out); //addr.sin_addr.s_addr = htonl(INADDR_ANY);// Time outstruct timeval tv;tv.tv_sec = 0;tv.tv_usec = 200000; // 200 mssetsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(struct timeval));// Bind 端口,用来接受之前设定的地址与端口发来的信息,作为接受一方必须bind端口,并且端口号与发送方一致if (bind(sockfd, (struct sockaddr*)&addr, addr_len) == -1){printf("Failed to bind socket on port %d\n", port_out);close(sockfd);return false;}char buffer[128];memset(buffer, 0, 128);int counter = 0;while(1){struct sockaddr_in src;socklen_t src_len = sizeof(src);memset(&src, 0, sizeof(src));int sz = recvfrom(sockfd, buffer, 128, 0, (sockaddr*)&src, &src_len);if (sz > 0){buffer[sz] = 0;printf("Get Message %d: %s\n", counter++, buffer);}else{puts("timeout");}}close(sockfd);return 0;
}
发送端:
#include<sys/select.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <iostream>int main(){int port_in = 12321;int port_out = 12322;int sockfd;// 创建socketsockfd = socket(AF_INET, SOCK_DGRAM, 0);if(-1==sockfd){return false;puts("Failed to create socket");}// 设置地址与端口struct sockaddr_in addr;socklen_t addr_len=sizeof(addr);memset(&addr, 0, sizeof(addr));addr.sin_family = AF_INET; // Use IPV4addr.sin_port = htons(port_in); //addr.sin_addr.s_addr = htonl(INADDR_ANY);// Time outstruct timeval tv;tv.tv_sec = 0;tv.tv_usec = 200000; // 200 mssetsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof(struct timeval));// 绑定获取数据的端口,作为发送方,不绑定也行if (bind(sockfd, (struct sockaddr*)&addr, addr_len) == -1){printf("Failed to bind socket on port %d\n", port_in);close(sockfd);return false;}int counter = 0;while(1){addr.sin_family = AF_INET;addr.sin_port = htons(port_out);addr.sin_addr.s_addr = htonl(INADDR_ANY);sendto(sockfd, "hello world", 11, 0, (sockaddr*)&addr, addr_len);printf("Sended %d\n", ++counter);sleep(1);}close(sockfd);return 0;
}
设置一个端口实现互通:
接收端:
//只有在server接收到消息后才能实现互发数据
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#define SERVER_PORT 8888//唯一端口号
int main(){int ser_sockfd;int len;fd_set rfds;socklen_t addrlen;char seraddr[100];struct sockaddr_in ser_addr;int retval, maxfd;//建立socketser_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(ser_sockfd<0){printf("I cannot socket success\n");return 1;}//设置地址与端口addrlen=sizeof(struct sockaddr_in);bzero(&ser_addr,addrlen);ser_addr.sin_family=AF_INET;ser_addr.sin_addr.s_addr=htonl(INADDR_ANY);ser_addr.sin_port= htons (SERVER_PORT);//server绑定,才能接受client的数据if(bind(ser_sockfd,(struct sockaddr *)&ser_addr,addrlen)<0){printf("connect");return 1;}while(1){bzero(seraddr,sizeof(seraddr));len=recvfrom(ser_sockfd,seraddr,sizeof(seraddr),0,(struct sockaddr*)&ser_addr,&addrlen);/*显示client端的网络地址*/printf("receive from %s\n",inet_ntoa(ser_addr.sin_addr));/*显示客户端发来的字串*/printf("recevce:%s",seraddr);/*输入字串返回给client端*/while(1){/*把可读文件描述符的集合清空*/FD_ZERO(&rfds);/*把标准输入的文件描述符加入到集合中*/FD_SET(0, &rfds);maxfd = 0;/*把当前连接的文件描述符加入到集合中*/FD_SET(ser_sockfd, &rfds);/*找出文件描述符集合中最大的文件描述符*/if(maxfd < ser_sockfd)maxfd = ser_sockfd;retval = select(maxfd+1, &rfds, NULL, NULL, NULL);if(FD_ISSET(ser_sockfd,&rfds))//client发消息来会出发进入{len=recvfrom(ser_sockfd,seraddr,sizeof(seraddr),0,(struct sockaddr*)&ser_addr,&addrlen); printf("recevce:%s",seraddr);}if(FD_ISSET(0, &rfds))//键盘输入会触发进入{len=read(STDIN_FILENO,seraddr,sizeof(seraddr));sendto(ser_sockfd,seraddr,len,0,(struct sockaddr*)&ser_addr,addrlen);}}}return 0;
}
发送端:
#include <netinet/in.h>
#include <string.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <arpa/inet.h>
#define SERVER_PORT 8888 //唯一端口
int main(int argc,char **argv){int cli_sockfd;int len;fd_set rfds;socklen_t addrlen;struct sockaddr_in cli_addr;char buffer[256];char buffer1[256];int retval, maxfd;//创建socketcli_sockfd=socket(AF_INET,SOCK_DGRAM,0);if(cli_sockfd<0){printf("I cannot socket success\n");return 1;}//配置地址与端口addrlen=sizeof(struct sockaddr_in);bzero(&cli_addr,addrlen);cli_addr.sin_family=AF_INET;cli_addr.sin_addr.s_addr=htonl(INADDR_ANY);//任何主机地址cli_addr.sin_port=htons(SERVER_PORT);//这里作为发送方,不需要绑定bind()while(1){bzero(buffer,sizeof(buffer));/* 从标准输入设备取得字符串*/ len=read(STDIN_FILENO,buffer,sizeof(buffer));/* 将字符串传送给server端*/sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);/* 接收server端返回的字符串*/while(1){/*把可读文件描述符的集合清空*/FD_ZERO(&rfds);/*把标准输入的文件描述符加入到集合中*/FD_SET(0, &rfds);maxfd = 0;/*把当前连接的文件描述符加入到集合中*/FD_SET(cli_sockfd, &rfds);/*找出文件描述符集合中最大的文件描述符*/if(maxfd < cli_sockfd)maxfd = cli_sockfd;retval = select(maxfd+1, &rfds, NULL, NULL, NULL);if(FD_ISSET(cli_sockfd,&rfds))//server发来数据将会触发进入循环{len=recvfrom(cli_sockfd,buffer1,sizeof(buffer1),0,(struct sockaddr*)&cli_addr,&addrlen);printf("receive: %s",buffer1);}if(FD_ISSET(0, &rfds))//键盘输入会触发进入{len=read(STDIN_FILENO,buffer,sizeof(buffer));sendto(cli_sockfd,buffer,len,0,(struct sockaddr*)&cli_addr,addrlen);}}}close(cli_sockfd);return 0;
}
以上。