赞
踩
进程通信的首要问题是如何唯一标识一个进程
因此,通过三元组(ip地址,协议,端口)就可以唯一标识网络中的进程了
使用TCP/IP协议的应用程序通常采用应用程序接口UNIX BSD的套接字(socket)来实现网络进程之间的通信,本文中的程序便是采用socket来实现客户端与服务器之间的通信
socket指一种在两端(一般是服务器-客户端)之间建立连接、传输数据的一种方式或一种约定(接口)
socket起源于Unix,Unix/Linux的特点是把文件作为不分任何记录的字符流进行存取,文件、文件目录和设备具有相同的语法语义和相同的保护机制,即“一切皆文件”,因此可以把socket看成是一种特殊的文件,socket函数则是对这种文件进行的操作(读写I/O、开启、关闭)
用于创建一个唯一标识的socket描述字,后续的操作以其为参数,通过它来进行一些读写操作,对应文件的打开操作
#include<WinSock2.h>
int socket(int domain,int type,int protocol);
/*domain:协议族,决定了socket的地址类型
常用的协议族有AF_INET(IPV4)、AFINET6(IPV6)、AF_LOCAL(本地通信)等
*/
/*
type:指定socket类型
常用的socket类型有SOCK_STREAM(面向连接的套接字,基于TCP协议)、SOCK_DGRAM(无连接的套接字,基于UDP协议)
*/
/*
protocol:指定协议
常用的协议有IPPROTO_TCP、IPPROTO_UDP、IPPRPTO_SCTP、IPPRPTO_TIPC
protocol为0时会自动选择type对应的默认协议
*/
socket()创建的socket描述字存在于协议族空间中,但没有一个具体地址,需要使用bind()函数给它赋一个地址
服务器需要通过调用bind()函数来绑定一个地址(如ip地址+端口号)使其能够被客户端连接并向客户端提供服务
客户端不需要指定,系统自动分配一个端口号和自身的ip地址结合
#include<WinSock2.h>
int bind(SOCKET sockfd,const struct sockaddr *addr,socklen_t addrlen);
/*
sockfd:socket描述字
addr:指向要绑定给sockfd的协议地址
addrlen:对应地址的长度
*/
其中需要注意,bind()中的addr需要转换成网络字节序
/* bind()之前对协议地址的设定 */
struct sockaddr_in addr:
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);// htons():把本地字节序改成网络字节序
addr.sin_addr.S_un.S_addr = ADDR_ANY;
不同的CPU有不同的字节序类型,分为大端字节序和小端字节序,网络字节序则为大端字节序,如果字节序类型不对应而未进行转换则会出现不可预知的问题,因此为保险起见建议本地字节序通过htons()转换成网络字节序后再赋给socket
#include<WinSock2.h>
int listen(int sockfd,int backlog);
int connect(SOCKET sockfd,const struct sockaddr *addr,socklen_t addr);
/*
backlog:相应socket可以排队的最大连接个数
*/
socket()创建的socket默认为主动类型的,listen()将socket变为被动类型的,等待客户的连接请求
客户端通过调用connect()来建立与TCP服务器的连接
connect()之后客户端向TCP发送了一个连接请求,TCP服务器监听到该请求后调用accept()接收请求,连接建立完成,之后可以进行网络I/O操作,类似文件的读写I/O操作
#include<WinSock2.h>
int accept(int sockfd,struct sockaddr *addr,socklen_t addrlen);
如果accept()成功,则返回值为一个由内核自动生成的一个全新的描述字,代表返回客户的TCP连接0
注意:accept()中的sockfd参数为服务器的socket描述字,是服务器调用socket()生成的,称为监听socket描述字,在服务器的生命周期中一直存在;accept()返回的socket描述字则是内核自动生成的已连接的描述字,仅仅在该客户端与服务器之间的连接过程中存在,当服务器完成了对该客户的服务之后就被关闭
#include<WinSock2.h>
int recv(SOCKET sockfd,char *buf,int len,int flags);
//buf:指向用于接收传入数据的缓冲区的指针,一般设定大小为1500字节(网络协议规定传输的最大单元)
//len:buf参数指向的缓冲区的长度(以字节为单位)
//flags:一组影响此函数行为的标志,一般置为0
若未发生错误,则recv()函数返回收到的字节数,buf指向的缓冲区将包含接收到的数据,如果连接正常关闭,则返回值为0,否则返回SOCKET_ERROR值,可以通过调用WSAGetLastError来检索特定的错误代码
#include<WinSock2.h>
int send(SOCKET sockfd,char *buf,int len,int flags);
//buf:指向包含要传输数据的缓冲区的指针
//len:buf参数指向的缓冲区的长度(以字节为单位)
若未发生错误,则send()函数将返回发送的总字节数,该字节数可能小于在len参数中请求发送的数量,否则将返回SOCKET_ERROR值,同样可以通过调用WSAGetLastError检索指定的错误代码
完成读写操作后,使用close()函数关闭socket描述字,对应文件的关闭操作
#include<unistd.h>
int close(int fd);
注意:close操作只是使相应socket描述字的引用次数减1,只有当引用计数器为0的时候,才会触发TCP客户端向服务器发送终止连接请求
TCP连接中的“三次握手”:
socket中“三次握手”发生的位置
注意:第三次握手,由于客户端已经处于established状态,因此可以携带数据,前两次握手是不能携带数据的
TCP连接中的“四次挥手”:
socket中的“四次握手”发生的位置:
本文中代码采用C++语法进行编写,但未采用面向对象的编程思想,因此代码结构仍然是封装成一个个功能函数,并未封装成对象,后续会自行改进,本文中就不贴出来了
在VS中创建解决方案,客户端和服务器为两个不同的项目用以分别运行进行测试
/*------------------------------tcpSocket.h------------------------------*/ #ifndef _TCPSOCKET_H_ #define _TCPSOCKET_H #include<stdbool.h> #include<iostream> #include<WinSock2.h> //头文件 #pragma comment(lib,"ws2_32.lib") //库文件 #pragma warning(disable:4996)//兼容问题,用于让VS忽略安全检查 #define err(errMsg) cout<<errMsg<<"failed,code "<<WSAGetLastError()<<" line:"<<__LINE__<<endl; #define PORT 8401 //0-1024为系统保留 //初始化网络库 bool init_Socket(); //关闭网络库 bool close_Socket(); //服务器:创建服务器socket SOCKET create_serverSocket(); //客户端:创建客户端socket SOCKET create_clientSocket(const char *ip); #endif
/*------------------------------tcpSocket.cpp------------------------------*/ #include<iostream> #include"tcpSocket.h" #include<WinSock2.h> using namespace std; bool init_Socket() { //初始化代码 WORD wVersion = MAKEWORD(2, 2); //MAKEWORD:将两个byte型合成一个word型,一个在高八位,一个在低八位 //MAKEWORD(1,1)只能一次接收一次,不能马上发送,只支持TCP/IP协议,不支持异步 //MAKEWORD(2,2)可以同时接收和发送,支持多协议,支持异步 WSADATA wsadata; if (0 != WSAStartup(wVersion, &wsadata)) //WSA:widows socket ansyc windows异步套接字 { err("WSAStartup"); return false; } //return true; } bool close_Socket()//反初始化操作 { if (0 != WSACleanup()) { err("WSACleanup"); } return true; } SOCKET create_serverSocket() { //1.创建一个空的socket //socket()无错误发生则返回引用新套接口的描述字,否则返回INVALID_SOCKET错误 SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (INVALID_SOCKET == fd) { err("socket"); return INVALID_SOCKET; } //AF_INET:指定地址协议族,INET指IPV4 //SOCK_STREAM:代表流式套接字 //IPPROTO_TCP:指定使用TCP/IP中的协议,此处指定使用TCP协议 //2.给socket绑定本地ip地址和端口号 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); //htons():把本地字节序转成网络字节序 addr.sin_addr.S_un.S_addr = ADDR_ANY; //绑定本地任意ip //3.bind绑定端口 if (SOCKET_ERROR == bind(fd, (struct sockaddr*)&addr, sizeof(addr))) { err("bind"); return INVALID_SOCKET; } //4.开始监听 listen(fd, 10); //同时允许10个用户进行访问 return fd; } SOCKET create_clientSocket(const char *ip) { //1.创建一个空的socket SOCKET fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (INVALID_SOCKET == fd) { err("socket"); return INVALID_SOCKET; } //2.给socket绑定服务端ip地址和端口号 struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(PORT); //htons():把本地字节序转成网络字节序 addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //绑定服务器ip,此处与本机另一进程通信,故ip为本机地址127.0.0.1 if (INVALID_SOCKET == connect(fd, (struct sockaddr*)&addr, sizeof(addr))) { err("connect"); return INVALID_SOCKET; } return fd; }
/*------------------------------fileOperation.h------------------------------*/ #ifndef __FILEOP_H_ #define __FILEOP_H_ #include"tcpSocket.h" #define _CRT_SECURE_NO_WARNINGS//兼容问题可能会报错,使用该行让VS忽略安全检测 /***服务端***/ //发送文件 bool sendFile(SOCKET s, const char* fileName); /***客户端***/ //接受文件 bool recvFile(SOCKET s, const char* fileName); #endif
/*------------------------------fileOperation.cpp------------------------------*/ #include"fileOperation.h" #include<iostream> using namespace std; long bufSize = 10*1024; //缓冲区大小 char* buffer; //缓冲区保存文件数据 //long recvSize = 10000000; //char* recvBuf; bool sendFile(SOCKET s, const char* fileName) { FILE* read = fopen(fileName, "rb"); if (!read) { perror("file open failed:\n");//输出描述性错误信息 return false; } //获取文件大小 fseek(read, 0, SEEK_END); //将文件位置指针移动到最后 bufSize = ftell(read); //ftell(FILE *stream):返回给定流stream的当前文件位置,获取当前位置相对文件首的位移,位移值等于文件所含字节数 fseek(read, 0, SEEK_SET); //将文件位置指针移动到开头 cout<<"filesize:"<<bufSize<< endl; //把文件读到内存中来 buffer= new char[bufSize]; cout << sizeof(buffer) << endl; if (!buffer) { return false; } int nCount; int ret = 0; while ((nCount = fread(buffer, 1, bufSize, read)) > 0) //循环读取文件进行传送 { ret += send(s, buffer, nCount, 0); if (ret == SOCKET_ERROR) { err("sendFile"); return false; } } shutdown(s, SD_SEND); recv(s, buffer, bufSize, 0); fclose(read); cout << "send file success!"<<" Byte:"<<ret << endl; system("pause"); return true; } bool recvFile(SOCKET s, const char* fileName) { if (buffer == NULL) { buffer = new char[bufSize]; if (!buffer) return false; } // 创建空文件 FILE* write = fopen(fileName, "wb"); if (!write) { perror("file write failed:\n"); return false; } int ret = 0; int nCount; while ((nCount = recv(s, buffer, bufSize, 0)) > 0) //循环接收文件并保存 { ret += fwrite(buffer,nCount, 1, write); } if (ret == 0) { cout << "server offline" << endl; } else if(ret < 0) { err("recv"); return false; } cout << "receive file success!" << endl; fclose(write); cout << "save file success! Filename:"<<fileName << endl; system("pause"); return true; }
#include"../tcpSocket/tcpSocket.h" #include"../tcpSocket/fileOperation.h" #include<iostream> #include<string> using namespace std; char* sendBuf; int main() { init_Socket(); SOCKET serfd = create_serverSocket();//创建服务器socket(该socket仅用于监听) cout << "server create success,wait client connect..." << endl; //等待客户端连接 sockaddr_in caddr; caddr.sin_family = AF_INET; int caddrlen = sizeof(sockaddr_in); SOCKET clifd = accept(serfd, (sockaddr*)&caddr, &caddrlen); //该socket用于与客户端进行连接 if (clifd == INVALID_SOCKET) { err("accept"); } cout << "connect success" << endl; //可以与客户端进行通信 char fileName[100] = { 0 }; cout << "please input the full path of the file: "<<endl; cin >> fileName; sendFile(clifd,fileName); closesocket(clifd); closesocket(serfd); close_Socket(); return 0; }
#include"../tcpSocket/tcpSocket.h" #include"../tcpSocket/fileOperation.h" #include<iostream> using namespace std; int main() { init_Socket(); SOCKET fd = create_clientSocket("127.0.0.1"); cout << "connect success!" << endl; //接收服务器传输的数据 char fileName[100] = { 0 }; cout << "input filename to save:" << endl; cin>>fileName; recvFile(fd, fileName); closesocket(fd); close_Socket(); return 0; }
- 使用 ifndef-define-endif
#ifndef xxx.h
#define xxx.h
...
#endif
- windows平台下可以使用如下代码:
#pragma once
在cpp文件中声明变量,然后建一个头文件在所有变量的声明前加上extern(此处不要对变量进行初始化),然后在其他需要使用全局变量的cpp文件中包含该头文件
单次传输文件时如果只使用一次send/recv函数,一旦文件大小超出缓冲区大小,则传输的文件会不全出现错误
解决方法:
发送和接收文件的时候将TCP传输的字节流分成多个TCP报文传输
while ((nCount = fread(buffer, 1, bufSize, read)) > 0) //循环读取文件进行传送
{
ret += send(s, buffer, nCount, 0);
if (ret == SOCKET_ERROR)
{
err("sendFile");
return false;
}
}
while ((nCount = recv(s, buffer, bufSize, 0)) > 0) //循环接收文件并保存
{
ret += fwrite(buffer,nCount, 1, write);
}
首先运行server.cpp,等待客户端程序连接
再运行客户端程序,服务器与客户端建立连接,此时在服务器进程选择server.cpp所处文件夹下的1.jpg作为发送文件
客户端程序client.cpp运行并连接成功后询问接收到的文件命名成什么,此处我选择命名成2.jpg
命名完文件后回车运行,文件保存成功后返回文件名
查看client文件夹中确实存在2.jpg且完整接收保存
本文中的代码也保存到了github上,代码链接
我对于socket网络编程这一块还只能算稍有了解,在学习过程中借鉴了许多文章,许多问题也都是网上查询找到的解决方案,实际的相关原理可能只是一知半解,撰写本文也主要是为了对于网络编程这一块有更深的理解,对项目内容有更全面的认识,故有一些理解不清晰或者理解存在问题的还望各位读者大佬不吝赐教
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。