OwenJi 2019-12-09
一、TCP/IP协议通信原理
使用TCP套接字编程可以实现基于TCP/IP协议的面向连接的通信,它分为服务器端和客户端两部分,其主要实现过程如下图所示:
(1)连接建立:服务器调用socket()、 bind()、 listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
(2)数据传输:建立连接后, TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。
(3)关闭连接:如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
二、函数描述
(1)socket函数:为了执行网络输入输出,一个进程必须做的第一件事就是调用socket函数获得一个文件描述符。
-----------------------------------------------------------------
#include <sys/socket.h>
int socket(int family,int type,int protocol);
返回:非负描述字---成功 -1---失败
-----------------------------------------------------------------
(2)connect函数:当用socket建立了套接口后,可以调用connect为这个套接字指明远程端的地址;如果是字节流套接口,connect就使用三次握手建立一个连接;如果是数据报套接口,connect仅指明远程端地址,而不向它发送任何数据。
-----------------------------------------------------------------
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr * addr, socklen_t addrlen);
返回:0---成功 -1---失败
-----------------------------------------------------------------
(3)bind函数:为套接口分配一个本地IP和协议端口,对于网际协议,协议地址是32位IPv4地址或128位IPv6地址与16位的TCP或UDP端口号的组合;如指定端口为0,调用bind时内核将选择一个临时端口,如果指定一个通配IP地址,则要等到建立连接后内核才选择一个本地IP地址。
-------------------------------------------------------------------
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr * server, socklen_t addrlen);
返回:0---成功 -1---失败
-------------------------------------------------------------------
(4)listen函数:listen函数仅被TCP服务器调用,它的作用是将用sock创建的主动套接口转换成被动套接口,并等待来自客户端的连接请求。
-------------------------------------------------------------------
#include <sys/socket.h>
int listen(int sockfd,int backlog);
返回:0---成功 -1---失败
-------------------------------------------------------------------
(5)accept函数:accept函数由TCP服务器调用,从已完成连接队列头返回一个已完成连接,如果完成连接队列为空,则进程进入睡眠状态。
-------------------------------------------------------------------
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *client, socklen_t * addrlen);
回:非负描述字---成功 -1---失败
-------------------------------------------------------------------
(6)write和read函数:当服务器和客户端的连接建立起来后,就可以进行数据传输了,服务器和客户端用各自的套接字描述符进行读/写操作。因为套接字描述符也是一种文件描述符,所以可以用文件读/写函数write()和read()进行接收和发送操作。
(I)write()函数用于数据的发送。
-------------------------------------------------------------------
#include <unistd.h>
int write(int sockfd, char *buf, int len);
返回:非负---成功 -1---失败
-------------------------------------------------------------------
(II)read()函数用于数据的接收。
-------------------------------------------------------------------
#include <unistd.h>
int read(int sockfd, char *buf, intlen);
回:非负---成功 -1---失败
-------------------------------------------------------------------
三、编程步骤及代码实现
《一》服务器端
编程步骤:
(1)使用WSAStartup()函数检查系统协议栈安装情况
(2)使用socket()函数创建服务器端通信套接字
(3)使用bind()函数将创建的套接字与服务器地址绑定
(4)使用listen()函数使服务器套接字做好接收连接请求准备
(5)使用accept()接收来自客户端由connect()函数发出的连接请求
(6)根据连接请求建立连接后,使用send()函数发送数据,或者使用recv()函数接收数据
(7)使用closesocket()函数关闭套接字(可以先用shutdown()函数先关闭读写通道)
(8)最后调用WSACleanup()函数结束Winsock Sockets API
代码实现:
#include <stdio.h> #include <windows.h> #pragma comment(lib,"ws2_32.lib")//WSAStartup、WSACleanup和SOCKET的动态库 int main() { //1 请求协议版本 WSADATA wsadata; WSAStartup(MAKEWORD(2,2), &wsadata);//请求协议版本2.2 if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2) { printf("请求协议失败\n"); return -1; } //2 创建socket SOCKET serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (SOCKET_ERROR == serverSocket) { printf("创建socket失败\n"); WSACleanup(); return -2; } printf("创建socket成功\n"); //3创建协议地址族 SOCKADDR_IN addr = { 0 }; addr.sin_family = AF_INET;//协议版本 addr.sin_addr.S_un.S_addr = inet_addr("192.168.43.3");//用自己IP addr.sin_port = htons(10086);//0-65535 一般取10000左右 //4 绑定 int r = bind(serverSocket, (sockaddr*)&addr, sizeof addr); if (r == -1) { printf("绑定失败\n"); closesocket(serverSocket); WSACleanup(); return -2; } printf("绑定成功\n"); //5 监听 r = listen(serverSocket, 10);//监听10个用户 if (r == -1) { printf("监听失败\n"); closesocket(serverSocket); WSACleanup(); return -2; } printf("监听成功\n"); //6 等待客户端连接 阻塞 尾声抱柱 SOCKADDR_IN cAddr = { 0 }; int len = sizeof cAddr; SOCKET clientSocket = accept(serverSocket, (sockaddr*)&cAddr, &len); if (SOCKET_ERROR == clientSocket) { printf("服务器宕机了\n"); //关闭socket closesocket(serverSocket); //清除协议信息 WSACleanup(); return -2; } printf("有客户端连接到服务器:%s\n", inet_ntoa(cAddr.sin_addr));//将整数型IP地址转化为字符串 // 7 通信 char buff[1024]; while (1) { r = recv(clientSocket, buff, 1023, NULL); if (r > 0) { buff[r] = 0;//添加‘\0‘ printf(">>%s\n", buff); } send(clientSocket, buf, sizeof(buf), 0); } return 0; }
《二》客户端
编程步骤:
(1)使用WSAStartup()函数检查系统协议栈安装情况
(2)使用socket()函数创建客户端套接字
(3)使用connect()函数发出也服务器建立连接的请求(调用前可以不用bind()端口号,由系统自动完成)
(4)连接建立后使用send()函数发送数据,或使用recv()函数接收数据
使用closesocet()函数关闭套接字
(5)最后调用WSACleanup()函数,结束Winsock Sockets API
代码实现:
#include <iostream> #include <WinSock2.h> #include <string> #pragma warning(disable:4996) #include <stdio.h> #include <windows.h> #pragma comment(lib,"ws2_32.lib")//WSAStartup、WSACleanup和SOCKET的动态库 int main() { //1 请求协议版本 WSADATA wsadata; WSAStartup(MAKEWORD(2, 2), &wsadata);//请求协议版本2.2 if (LOBYTE(wsadata.wVersion) != 2 || HIBYTE(wsadata.wVersion) != 2) { printf("请求协议失败\n"); return -1; } //2 创建socket SOCKET clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (SOCKET_ERROR == clientSocket) { printf("创建socket失败\n"); WSACleanup(); return -2; } printf("创建socket成功\n"); //3获取协议地址族 SOCKADDR_IN addr = { 0 }; addr.sin_family = AF_INET;//协议版本 addr.sin_addr.S_un.S_addr = inet_addr("192.168.43.3");//用自己IP addr.sin_port = htons(10086);//0-65535 一般取10000左右 //4 连接服务器 int r = connect(clientSocket, (sockaddr*)&addr, sizeof addr); if (r == -1) { printf("连接服务器失败\n"); return -1; } printf("连接服务器成功\n"); //5 通信 char buf[1024]; string str; int i; cin >> str; for (i = 0; i < str.length(); i++) { buf[i] = str[i]; } buf[i] = ‘\0‘; send(clientSocket, buf, sizeof(buf), 0); // 接收数据 recv(clientSocket, buf, sizeof(buf), 0); return 0; }
四、运行结果
1、数据传送之前先启动服务器端,再启动客户端:
启动服务器后,服务器界面如下,此时客户端还未启动或请求连接。
2、启动客户端后,与服务器的连接建立成功,两者界面如下图:
3、连接建立成功后,开始传输字符串聊天,结果如下:
网络聊天实验完成