一、通讯流程
1、确定两端(客户端/服务端)
2、双方约定通讯规则(协议)
3、各自赋予赋予地址(IP地址和端口)
- 服务端开启监听
- 客户端根据服务端计算机IP和服务端程序监听端口连接服务端
- 服务端决定是否准许客户端连接
4、建立连接(TCP需要)
5、发送和接收数据
6、处理数据
7、关闭连接
二、一些介绍
C/S架构,客户端/服务器端
内网外网通讯:外网是服务器端,内网客户端。
1、服务端
IP地址明确,可访问,端口号必须指定
2、客户端
IP可以是不明确的,端口号可以不确定,一般由套接字动态分配
3、通讯基础
通讯的双方是什么:程序(进程)(特殊的进程间通信)
通讯过程:封装(头部构造)——传输——拆封组装
实际操作:程序将数据写入网卡缓存——网卡传输数据——接收端从网卡缓存取数据
三、头文件及函数介绍(建议看官方文档)
了解了TCP通讯一般流程和基本要求,接下来开始介绍TCP网络编程所使用到的头文件和函数。
1、头文件(主要介绍Windows的winsock.h)
网络编程需要一个套接字来进行通讯,套接字能够帮我们构造包头,不需直接读写缓存,让我们不需要关心通讯细节。而这个套接字的头文件在不同系统中不一样(版本也不一样)。
(1)Linux
sys/types.h: 数据类型定义
sys/socket.h: 提供socket函数及数据结构
netinet/in.h: 定义数据结构sockaddr_in
arpa/inet.h: 提供IP地址转换函数
netdb.h: 提供设置及获取域名的函数
sys/ioctl.h: 提供对I/O控制的函数
sys/poll.h: 提供socket等待测试机制的函数
(2)Windows
#include <winsock.h> //winsock1
#include <winsock2.h> //winsock2
#include < Afxsock.h> //MFC套接字类
提示:
WinSock1提供的是与Unix/Linux类操作系统保持兼容的通用的、基本的函数库,为了让你的程序能够运行于大多数平台,最好使用Winsock1.1规范。
Win sock2兼容winsock1,Winsock2引入了与Windows内核密切相关的【重叠IO】机制,提供了WSA打头的,支持【重叠IO】的异步函数库 Winsock2还将一些常用socket操作序列进行了打包封装,提供了以Ex结尾的部分函数。
光是引用头文件还不能使用,需要加载动态链接库,分为隐式加载、显示加载(一般采用隐式加载)
(1)隐式加载
Winsock1
#pragma comment(lib, “wsock32.lib”) //隐式加载
#include <winsock.h>
Winsock2
#pragma comment(lib, "ws2_32.lib")
#include <winsock2.h>
AfxSocket(MFC socket封装)
#include <afxsock.h> //(已隐式加载)
(2)显示加载(windows1为例)
#include<windows.h> //必须包含 windows.h
typedef int (*FUNADDR)(); // 指向函数的指针
int main(){
HINSTANCE s= LoadLibrary(" WINSOCK.DLL ");//ws2_32.dll
FUNADDR socket;
if(s){
socket = (FUNADDR)GetProcAddress(s, “socket");
}
SOCKET mysock=socket(......);
......
}
说完需要的头文件,接下来介绍每个函数。在此之前需要初始化套接字(不介绍,后面代码有)。
2、socket()——创建套接字
socket (int af, int type, int protocol);
(1)Af:地址簇
(2)Type:套接字类型
(3)protocol:具体使用协议
(4)返回值:如果函数执行成功,返回值为一个整数,否则为 INVALID_SOCKET(0)。
服务端、客户端必须使用相同的地址簇、相同的套接字类型和相同的协议
常见的类型(最好看官方文档):
AF_INET //IPv4(TCP/UDP/etc.)
AF_INET6 //IPv6
SOCK_STREAM //流式套接字——这种套接字是有连接的,数据在客户端是顺序发送的,并且到达的顺序是一致的。发1、2、3……,收1、2、3……
SOCK_DGRAM //数据包套接字——这种套接字是无连接的,到达的顺序不一定与发送的顺序是一致的,并且数据不一定是可达的,并且接收到的数据还可能出错。发1、2、3、4……,收2、1、4……
SOCK_RAW //原始套接字——获取包头信息,或自己构造包头
IPPROTO_TCP // 6
IPPROTO_UDP // 17
示例:
SOCKET s=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) //建立TCP连接
SOCKET s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) //建立UDP连接
3、bind()——服务端绑定套接字
bind(SOCKET s, const sockaddr *name,int namelen);
(1)s:是一个套接字。
(2)name:是一个 sockaddr 结构指针,该结构中包含了要结合的地址和端口号。
(3)namelen:确定 addr 缓冲区的长度。
(4)返回值:如果函数执行成功,返回值为0,否则为SOCKET_ERROR(-1)。
难点在于 addr 结构指针,所以需要引出 TCP/UDP 专用地址书写(IPv4的)
struct sockaddr_in {
short int sin_family;
unsigned short int sin_port;
struct in_addr sin_addr;
unsigned char sin_zero[8]; //填充0 以保持与struct sockaddr同样大小
};
示例
sockaddr_in server_addr;
server_addr.sin_family = AF_INET; //允许IPv4连接
server_addr.sin_port = htons(SERVER_PORT); //连接端口
server_addr.sin_addr.s_addr = htonl(INADDR_ANY); //允许所有地址连接
bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr))
提示:
绑定内容:IP(网卡)、端口号
(1)服务端绑定:本机的IP和端口号
(2)客户端绑定:服务端IP和服务端程序程序端口号
注意:
(1)服务器端和客户端绑定内容必须相符。
(2)服务器端绑定相同IP相同端口的程序不能同时运行,会冲突出错。
(3)客户端不同程序可以连接相同IP和端口
(4)客户端不用绑定本地IP和端口号(为什么)
- 默认为本机IP
- 自动分配端口(避开冲突)
- 连接成功后,服务器端能够获取客户端IP和端口
(5)设置端口是避开常用端口号,建议>5000
4、listen()——监听
listen( SOCKET s, int backlog);
(1)s:用于标识一个已捆绑未连接套接口的描述字。
(2)backlog:等待连接队列的最大长度。(同时准许多少个客户端连接)
(3)返回值:如果函数执行成功,返回值为0,否则为SOCKET_ERROR(-1)。
示例:
listen(serverSock,8);
提示:
服务器端开始监听后客户端才能开始连接
5、connect()——客户端连接服务端
connect(SOCKET s, const sockaddr *name,int namelen);
(1)s:是一个套接字。
(2)name:是一个 sockaddr 结构指针,该结构中包含了要结合的地址和端口号。
(3)namelen:确定 addr 缓冲区的长度。
(4)返回值:如果函数执行成功,返回值为0,否则为SOCKET_ERROR(-1)。
与绑定函数同样使用,不过是客户端连接服务端信息
示例:
sockaddr_in server_socket;
server_socket.sin_family = AF_INET;
server_socket.sin_port = htons(SERVER_PORT);
server_socket.sin_addr.s_addr = inet_addr(SERVER_IP);
connect(ljf_client_socket, (SOCKADDR*)&server_socket, sizeof(server_socket));
提示:
Connect函数是阻塞的,会反复尝试,直到超时。Connect成功的连接包括三次握手信号。
6、accept()——服务端接受连接请求
accept( SOCKET s, struct sockaddr *addr,int *addrlen);
(1)s:监听套接字
(2)addr:获取客户端地址结构
(3)Addrlen:地址类型长度
(4)返回值:新套接字,数据传送由该套接字继续完成,监听套接字继续监听,错误为 INVALID_SOCKET(0)
示例:
sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
//等待建立连接
SOCKET client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_len);
7、send()——发送数据
send( SOCKET s, const char *buf, int len, int flags);
(1)s:发送套接字。
(2)buf:包含待发送数据的缓冲区。
(3)len:缓冲区中数据的长度。
(4)flags:调用执行方式(建议都写0)。
示例:
char buf[] = "hello";
int lbuf = strlen(buf);
send(s,buf,lbuf,0);
注意:
(1)连接成功后,任意端可向另一端发送数据,不分先后
(2)服务器端必须用accept产生的新套接字发送和接收数据
(3)必须计算清楚发送字符的长度
8、recv()——接收数据
recv( SOCKET s, char *buf, int len, int flags);
(1)s:发送套接字。
(2)buf:包含待发送数据的缓冲区。
(3)len:缓冲区中数据的长度。
(4)flags:调用执行方式(建议都写0)。
(5)返回值:成功:>0,接收数据长度;出错:0,未接收到数据,负数,关闭连接。
示例:
char buf[1024];
int lbuf = 1024;
lbuf = recv(s,rbuf,lbuf,0);
buf[lbuf] = '\0';
注意:
(1)接收数据的缓冲区可以设得略大
(2)根据实际接收长度,添加字符串结束标志,但是可以每次清空缓冲区(全是结束标志,只添加有用信息)
9、closesocket()——关闭套接字
closesocket( SOCKET s);
注意:
(1)任意端可以先关闭连接 ,但两边都必须自行关闭
(2)关闭后释放套接字连接
四、网络编程——简单TCP套接字编程
仅实现单个客户端连接发送数据,且服务端和客户端使用 winsock.h
1、服务端——server.cpp
/*****************************************************************************
* @author : ljf *
* @date : 2020/03/17 *
* @file : Server.cpp *
* @brief : 基于TCP协议通信——服务端 *
*----------------------------------------------------------------------------*
* Change History *
*----------------------------------------------------------------------------*
* Date | Version | Author | Description *
*----------------------------------------------------------------------------*
* 2020/03/17 | 1 | ljf | 创建并简单实现 *
*****************************************************************************/
#include <stdio.h>
#include <string.h>
#include <winsock.h>
#pragma comment(lib, "wsock32.lib") //隐式加载
int main() {
//服务器监听端口
int SERVER_PORT = 5678;
//缓冲区大小
const int BUFF_SIZE = 1024;
char SEND_BUFF[] = "已收到";
char RECV_BUFF[BUFF_SIZE];
//初始化套接字
WSADATA wsadata;
if (0 != WSAStartup(MAKEWORD(1, 1), &wsadata)) {
printf("WSAStartup error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -1;
}
//创建服务端套接字(监听套接字)
SOCKET server_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == server_socket) {
printf("Create socket error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -2;
}
//初始化监听信息
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(SERVER_PORT);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (SOCKET_ERROR == bind(server_socket, (sockaddr*)&server_addr, sizeof(server_addr))) {
printf("socket bind error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -3;
}
//监听数目(暂时没用,阻塞)
if (SOCKET_ERROR == listen(server_socket, 5)) {
printf("socket listen error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -4;
}
printf("Listen port %d...\n", SERVER_PORT);
sockaddr_in client_addr;
int client_addr_len = sizeof(client_addr);
//等待建立连接
SOCKET client_socket = accept(server_socket, (sockaddr*)&client_addr, &client_addr_len);
if (INVALID_SOCKET == client_socket) {
printf("socket accept error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -5;
}
else {
printf("[Connect]: %s:%d\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
//信息交互
while (true) {
//清空缓冲区
memset(RECV_BUFF, 0, BUFF_SIZE);
//接收信息
if (0 < recv(client_socket, RECV_BUFF, BUFF_SIZE, 0)) {
printf("[Recv]: %s\n", RECV_BUFF);
send(client_socket, SEND_BUFF, strlen(SEND_BUFF), 0);
}
//断开连接(客户端排除数据长度0)
else {
break;
}
}
closesocket(client_socket);
closesocket(server_socket);
WSACleanup();
system("pause");
return 0;
}
2、客户端——client.cpp
/*****************************************************************************
* @author : ljf *
* @date : 2020/03/17 *
* @file : Client.cpp *
* @brief : 基于TCP协议通信——客户端 *
*----------------------------------------------------------------------------*
* Change History *
*----------------------------------------------------------------------------*
* Date | Version | Author | Description *
*----------------------------------------------------------------------------*
* 2020/03/17 | 1 | ljf | 创建并简单实现 *
*****************************************************************************/
#include <stdio.h>
#include <string.h>
#include <winsock.h>
#pragma comment(lib, "wsock32.lib") //隐式加载
int main() {
//服务器端口
int SERVER_PORT = 5678;
//服务器IP
char SERVER_IP[20] = "127.0.0.1";
//缓冲区大小
const int BUFF_SIZE = 1024;
char SEND_BUFF[BUFF_SIZE];
char RECV_BUFF[BUFF_SIZE];
//初始化套接字
WSADATA wsadata;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsadata)) {
printf("WSAStartup error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -1;
}
//创建客户端套接字
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (INVALID_SOCKET == client_socket) {
printf("Create socket error ---- Error Code: %d", WSAGetLastError());
WSACleanup();
return -2;
}
//初始化服务端连接信息
sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(5678);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
//连接到服务端
if (SOCKET_ERROR == connect(client_socket, (sockaddr*)&server_addr, sizeof(server_addr))) {
printf("Connect %s:%d failed ---- Error Code: %d\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port), WSAGetLastError());
WSACleanup();
return -3;
}
else {
printf("[Connect %s:%d success]\n", inet_ntoa(server_addr.sin_addr), ntohs(server_addr.sin_port));
}
//信息交互
while (true) {
//清空缓冲区
memset(SEND_BUFF, 0, BUFF_SIZE);
memset(RECV_BUFF, 0, BUFF_SIZE);
printf("[Send]: ");
//读取一行字符串,回车结束,回车会被读入
fgets(SEND_BUFF, BUFF_SIZE, stdin);
//处理末尾的回车
SEND_BUFF[strlen(SEND_BUFF) - 1] = '\0';
//收发数据
if (strlen(SEND_BUFF) != 0) {
send(client_socket, SEND_BUFF, strlen(SEND_BUFF), 0);
recv(client_socket, RECV_BUFF, BUFF_SIZE, 0);
printf("[Recv]: %s\n", RECV_BUFF);
}
}
closesocket(client_socket);
WSACleanup();
system("pause");
return 0;
}
五、结果
六、总结
接下来实现多线程多客户端连接