网络编程——简单TCP套接字编程

一、通讯流程

1、确定两端(客户端/服务端)
2、双方约定通讯规则(协议)
3、各自赋予赋予地址(IP地址和端口)

  1. 服务端开启监听
  2. 客户端根据服务端计算机IP和服务端程序监听端口连接服务端
  3. 服务端决定是否准许客户端连接

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和端口号(为什么)

  1. 默认为本机IP
  2. 自动分配端口(避开冲突)
  3. 连接成功后,服务器端能够获取客户端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;
}

五、结果

在这里插入图片描述

六、总结

接下来实现多线程多客户端连接

  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值