SOCKET 实现TCP通信
学习知识对知识框架的把握是十分重要的,要知道学的知识点处于框架的哪个位置
所以对于网络知识,知识体系和框架非常重要,目前公认的网络模型有两种,一种是OSI七层模型,另一种是TCP/IP五层模型。对应关系如下
从上图中可以看出,OSI模型与TCP/IP模型之间是存在对应关系的,并且以传输层为分界线,传输层及以下完全一致,TCP/IP的网络接口层就是数据链路层和物理层的集合。
上述两个模型均为常用模型,但是相对而言,TCP/IP更为普遍。TCP中不同的层对应不同的协议
应用层:FTP(文件传输协议) , DNS ,HTTP , Telent , SMTP,
传输层: TCP UDP
网络层:IP ARP(地址解析协议) RARP ICMP(互联网控制信息协议)
数据链路层: 各种通信网络接口,以太网,物理网络等。
TCP协议的三次握手和四次挥手
所谓的三次握手和四次挥手,是指TCP连接和断开的过程。三次握手即连接需要三次数据交互的确定。四次挥手即指TCP断开时需要四次数据交互的确定。
以打电话为例:
三次握手:A给B打
A:喂,是B吗?
B:是的是的,你是A吗?
A:是的,是的,开始聊天吧
四次挥手:
A:今天就结束了吧。
B:好的
B:其实我也想结束
A:那就结束吧。
这里边值得注意的是,在四次挥手的时候A先提出了断开的请求,但实际断开并没有结束,在B确定了A的断开请求后,依然在继续对话。这是由于TCP的全双工(Full Duplex)传输特性。就是说虽然只有一个连接,但是存在A-B,B-A 两条路,就和双行公路是一样的。A发出断开请求,只是A到B的路断了,但是B到A 的路还通着。直到B提出断开请求,并且A确定后,连接才算彻底断开。
TCP的连接建立过程中是分为客户端和服务端的。通俗来讲,就就是主动端和被动端,服务器是被动端,只能等着别人来连接。客户端进行主动连接,连接的方式就是通过IP地址和端口号,一个服务器可以接受多个客户端的连接,但是一个客户端只能连接一台服务器。连接后服务端会自动划分内存区域以分配客户端的通信。至于端口,IP,内存的关系,如下图所示:
程序实现过程
TCP/IPP协议:
1. 叫传输控制/网际协议,又叫网络通信协议,
2. 套接字socket
在网络中用来描述计算机中不同程序与其他计算机程序的通信方式。socket其实是一种特殊的IO接口。也是一种文件描述符。套接字socket分为三类
流式socket(SOCK_STREAM):流式套接字提供可靠、面向连接的通信流,使用TCP协议,保证了数据的时序性和正确性。
数据报socket(SOCK_DGRAM):数据报套接字定义的是无连接服务,数据相对独立传输,不能保证时序性与正确性,使用数据包协议的是UDP。
原始socket:原始套接字允许对底层协议如IP或者ICMP进行访问
套接字由三个参数组成:IP地址,端口号,传输层协议。用以上三个参数判断不同进程间的网络通信与链接。
3. 套接字数据结构:
C语言编写socket程序时,通常会用到sockaddr和sockaddr_in两个结构体,用来保存套接字信息
结构体具体内容如下:
其中,对于sockaddr,一般编程中不会对此结构体进行操作,大部分情况用于bind,connect,sendto,recvfrom等函数的参数,会用sockaddr_in来代替。
常见的地址族有:AF_INET, AF_INET6AF_LOCAL
端口以及地址处理函数
(1)unit16_t_hons(unit16_t_host16bit)把16位值从主机字节序转到网络字节
(2)unit32_t_hons(unit32_t_host32bit)把32位值从主机字节序转到网络字节
(3)unit16_t_ntohs(unit16_t_net16bit)把16位值网络字节序转到主机字节序
(4)unit32_t_ntohs(unit32_t_net32bit)把32位值网络字节序转到主机字节序
4. 用socket进行TCP通信是常用函数以及所在头文件
(1)socket :建立一个socket连接
#include <sys/types.h>
#include<sys/socket.h>
函数原型:int socket(int domain,int type,int protocol)
domain :协议类型,一般为AF_INET
type:socket类型
protocol:用来指定socket所使用的的协议编号,通常设置为0即可
(2)bind:将socket与本机上的一个端口绑定,之后就可以在端口监听服务请求
#include <sys/types.h>
#include<sys/socket.h>
函数原型:int bind (int sockfd,struct sockaddr*my_addr,int addr_len)
sockfd :socket创建返回值,或者叫描述符
my_addr:是一个指向包含有本机地址IP和端口号等信息的sockaddr类型的指针
addr_len:常被设置为sizeof(struct sockaddr)
(3)connect :面向连接的客户程序使用connect函数来配置socket,并与远端服务器建立TCP连接
#include <sys/types.h>
#include<sys/socket.h>
函数原型:int connect(int sockfd,struct sockaddr*serv_addr,int add_len)
sockfd :socket创建返回值,或者叫描述符
serv_addr:包含目的机器的地址和IP的端口的sockaddr类型指针
addr_len:常被设置为sizeof(struct sockaddr)
(4)listen :将socket处于被动监听状态,并为该socket建立一个输入数据队列,将到达的服务器请求保存在此队列中,直到程序处理他们
#include<sys/socket.h>
函数原型 int listen(int sockfd,int backlog)
sockfd :socket创建返回值,或者叫描述符
backlog: 指定在请求队列中的最大请求数,进入的连接请求将在队列中等待accept()它们。
(5)accept :让服务器接收客户端的连接请求
#include <sys/types.h>
#inlcude <sys/socket.h>
函数原型: int accept(int sockfd, void *addr, int addrlen)
sockfd:是被监听的socket描述符
addr:通常是一个指向sockaddr_in变量的指针,该变量用来存放提出连接请求服务的主机的信息
addrlen:sizeof(struct sockaddr_in)
(6)close :停止在socket上进行数据传输操作
(7)send:
(8)recv:
C语言实现TCP连接的服务端与客户端程序
程序调试过程中遇到的问题
- error C1083: 无法打开包括文件: “sys/socket.h”:,即VS无法找到socket 头文件
原因:Windows系统下没有这个头文件,
解决方法:改用<windows.h>,<winsock2.h> - error C2011: “sockaddr”:“struct”类型重定义
原因:有2种情况(1)有变量名相同
(2)出现在头文件中,有头文件中类型重复定义
解决方法:增加预编译语句。 具体方法将#include <windows.h>放在#include<winsock2.h>之后即可。 - 问题:关闭socket时没有close函数
可能在头文件中定义成了closesocket(Socket) - error LNK2019: 无法解析的外部符号 __imp__accept@12,该符号在函数 _main 中被引用
在界面中可以找到看到定义的函数,但是编译却出现错误,提示无法解析符号
解决方法,在程序中加入#pragma comment(lib,“Ws2_32.lib”) - socket 创建一直失败,返回值一直是-1
在Windows调试中,出现返回值总为-1的情况需要加几行代码,在Windows中使用socket需要先注册,注册代码如下:
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(1, 1);
err = WSAStartup(wVersionRequested, &wsaData);
if(err != 0)
{
perror(“WSAStartup error”);
} - 连接时,connect失败,可能是两电脑IP不在同一个域
程序
1.客户端
/ TCP_test_client.cpp : 定义控制台应用程序的入口点。169
//
#include “stdafx.h”
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib,“ws2_32.lib”)
//定义TCP参数
#define CLIENT_PORT 8080
#define SERVER_ADDR “192.168.5.2”
int main(int )
{
//进行Windows使用socket前的注册
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(1,1);
err = WSAStartup(wVersionRequested,&wsaData);
if(err!=0)
{
perror(“WSAStartup error”);
}
SOCKET clientSocket;
struct sockaddr_in serverAddr;
int dataNum;
char sendbuf[200] = {0};
clientSocket = socket(AF_INET,SOCK_STREAM,0);
if(clientSocket < 0)
{
printf("socket creat failed!\n");
}
else
{
printf("socket creat success!\n");
}
//初始化
serverAddr.sin_family = AF_INET;
serverAddr.sin_port= htons(CLIENT_PORT);
serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);//指定服务器的IP
SOCKET connectfd ;
while(1)
{
connectfd = connect(clientSocket,(struct sockaddr * )&serverAddr,sizeof(serverAddr));
if(connectfd < 0)
{
printf("connect failed\n");
printf("connectfd = %d\n",connectfd);
}
else
{
printf("connect success!\n");
}
dataNum = send(clientSocket,sendbuf,sizeof(sendbuf),0);
printf("%d\n",dataNum);
if(dataNum < 0)
{
printf("data send error !\n");
}
}
getchar();
return 0;
}
2. 服务端
// stdafx.cpp : 只包括标准包含文件的源文件
// TCP_test_sever.pch 将作为预编译头
// stdafx.obj 将包含预编译类型信息
#include “stdafx.h”
// TODO: 在 STDAFX.H 中
// 引用任何所需的附加头文件,而不是在此文件中引用
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include
#include <winsock2.h>
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib,“ws2_32.lib”)
#define SERVER_PORT 8080
#define SERVER_ADDR “192.168.1.133”
#define PACKAGE_SIZE 1024
int main()
{
//先注册
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(1,1);
err = WSAStartup(wVersionRequested,&wsaData);
if(err!=0)
{
perror("WSAStartup error!");
}
//变量定义
SOCKET serverSocket;
SOCKET client;
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;
char buffer[200];
int dataNum = 0;
int addr_len = sizeof(client_addr);
int listenfd = 0;
//socket创建
serverSocket = socket(AF_INET,SOCK_STREAM,6);
if(serverSocket < 0)
{
printf("socket creat failed !\n");
//printf("%d",serverSocket);
}
else
{
printf("socket creat success!\n");
}
//服务信息
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);//ip可以是本服务器的IP,也可以用宏INADDR_ANY代替,代表0.0.0.0,表明所有地址
//socket绑定本地
int bindfd = 0;
bindfd = bind(serverSocket,(struct sockaddr *)&server_addr,sizeof(server_addr));
if(bind < 0)
{
printf("socket bind failed !\n");
}
else
{
printf("socket bind success!\n");
}
//监听状态
listenfd = listen(serverSocket,2);
if(listen < 0)
{
printf("listen error!\n");
}
printf("%d\n",listenfd);
printf("监听端口: %d\n",SERVER_PORT);
int asc = 200;
printf("4897\n");
while(1)
{
client = accept(serverSocket,(struct sockaddr*)&client_addr,(socklen_t *)&addr_len);
printf("1\n");
if(client < 0)
{
printf("accept error\n");
}
else
{
printf("accept success\n");
}
asc = 200;
while(asc)
{
//printf("1\n");
dataNum = recv(client,buffer,200,0);
//printf("2\n");
if(dataNum < 0)
{
printf("data recv error!\n");
}
printf("%d\n",dataNum);
asc -= dataNum;
//closesocket(client);
}
//closesocket(client);
printf("852\n");
}
closesocket(serverSocket);
}