做网络通信作业之前的学习 !(>。<)!
一.TCP
1.服务端流程
1.创建socket套接字
socket套接字可以理解成网络接口,只有通过了socket套接字才能跟对应的电脑进行通信
2.给这个socket绑定一个端口号
IP地址是指定电脑的 端口号是指定电脑上面某个软件的
3.给socket开启监听属性
这个socket只能用来接收连接 不能用来做通讯
4.等待客户端连接
5.开始通讯
6.关闭连接
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
//windows上使用网络功能需要开始网络权限
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//1.创建socket套接字
/*socket(
int af, //协议地址簇 ipv4/ipv6 对应 AF_INET/AF_INET6
int type, //类型 流式协议/帧式协议 对应 SOCK_STREAM/SOCK_DGRAM
int protocol //保护协议 tcp/udp不用填保护协议 直接填0
);*/
//此处是服务端socket,所以我们一般起名叫监听socket
SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
//SOCKET是一个无符号的长整型,就是一个整数,可以直接打印出来
//未开启网络结果为-1 即无效的socket -> INVALID_SOCKET
if (INVALID_SOCKET == listen_socket){
printf("create listen socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//2.给这个socket绑定一个端口号
//struct sockaddr_in {
// ADDRESS_FAMILY sin_family; //协议地址簇
// USHORT sin_port; //端口号
// IN_ADDR sin_addr; //IP地址
// CHAR sin_zero[8]; //保留字节 -> 协议升级会用
//};
//大小端问题:
// 一个数字在计算机中存储以二进制存储,对端口来讲占两个字节
// 编程中最小单位我们通常用字节来算,很少用位算
// 存数据的时候会有先后的问题 8080 -> 1F90(大端序号,高位放前面【千百个十】)
// 但是本地电脑的存储方式是以小端序存储的【个十百千】
//unsigned short a = 8080;
//unsigned short* p = &a;
struct sockaddr_in local = { 0 };
local.sin_family = AF_INET;
local.sin_port = htons(8080); //大小端问题 中间设备使用的是大端序
//服务端 选项 网卡 127.0.0.1(本地环回)只接受哪个网卡的数据
//一般写0.0.0.0 不管哪个数据库来 只要有我都接受
//INADDR_ANY整数 占4个字节
//local.sin_addr.s_addr = htonl(INADDR_ANY);
//手动指定
local.sin_addr.s_addr = inet_addr("0.0.0.0"); //字符串IP转换成整数IP
//绑定 给上面定义的socket绑定我们指定的内容
/*int bind(
SOCKET s, //对哪个socket进行绑定
const struct sockaddr FAR * name, //sockaddr的结构
int namelen //长度
); 返回整数类型*/
//为什么使用sockaddr结构而不是sockaddr_in结构?(两个结构体的大小一模一样)
/*struct sockaddr {
ADDRESS_FAMILY sa_family;
CHAR sa_data[14];
} 用这个方便扩展 通用的结构*/
if (-1 == bind(listen_socket, (struct sockaddr*)&local, sizeof(local))) {
printf("bind socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//3.给socket开启监听属性
/*int listen(
SOCKET s,
int backlog //半连接队列长度
);*/
if (-1 == listen(listen_socket, 10)) {
printf("start listen socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//以上准备工作结束
//4.等待客户端连接
//返回的客户端socket才是跟客户端可以通讯的一个socket
//listen_socket唯一的作用就是等待连接 最后返回一个socket出来
//accept()是阻塞函数,等到有客户端连接进来就接受连接,然后染回,否则就阻塞
/*SOCKET accept(
SOCKET s, //监听socket
struct sockaddr * addr, //客户端的IP地址和端口号
int * addrlen //结构的大小 为什么是指针 因为可填可不填 要填就要和上面的addr都填
);*/
//如果要跟多个客户端通讯 需要有while循环
while (1)
{
SOCKET client_socket = accept(listen_socket, NULL, NULL);
if (INVALID_SOCKET == client_socket)
continue;
//5.开始通讯(B/S)
//通过浏览器访问一个网站的时候
//浏览器会发送一个http请求 http是tcp的上层协议 所以写tcp是可以接收http的报文的
//接收发送的报文
char buffer[1024] = { 0 };
/*int recv(
SOCKET s, //客户端socket
char* buf, //接受的数据存到哪里
int len, //接受的长度
int flags //0
);*/
recv(client_socket, buffer, 1024, 0);
printf("%s\n", buffer);
//6.关闭连接
closesocket(client_socket);
}
return 0;
}
错误码查找
大小端问题
中间设备使用的是大端序(路由器) 要从主机转换成中间件能使用的 -> htons()
不安全问题:把安全开发关掉
程序一旦弹出
代表监听已经开启了
模拟时输入127.0.0.1:8080 可以收到数据 --> tcp连接没问题
2.客户端流程
1.创建socket套接字(通过套接字连接到服务端)
2.连接服务器
3.开始通讯
4.关闭连接
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//1.创建socket套接字(通过套接字连接到服务端)
SOCKET client_socket = socket(AF_INET, SOCK_STREAM, 0);
if (INVALID_SOCKET == client_socket) {
print("create socket failed!!!\n");
return -1;
}
//2.连接服务器
struct sockaddr_in target;
target.sin_family = AF_INET;
target.sin_port = htons(8080);
target.sin_addr.s_addr = inet_addr("127.0.0.1");
if (-1 == connect(client_socket, (struct sockaddr*)&target, sizeof(target)))
{
printf("connet server failed !!!\n");
closesocket(client_socket);
return -1;
}
//3.开始通讯 send recv
//回显服务器 -> 发送什么会回复什么
while (1)
{
char sbuffer[1024] = { 0 };
printf("please enter: ");
scanf("%s", sbuffer);
send(client_socket, sbuffer, strlen(sbuffer), 0);
//立马接收
char rbuffer[1024] = { 0 };
int ret = recv(client_socket, rbuffer, 1024, 0);
if (ret <= 0) break;
printf("%s\n", rbuffer);
}
//4.关闭连接
closesocket(client_socket);
return 0;
}
tcp服务器客户端的重点在于服务端
因为存在的问题比较多:发送的消息怎么进行处理,如何同时接收多个客户端进行连接
3.实现多线程的客户端
服务端不应该主动断开连接而是由客户端断开
//连接一旦完成进入线程
//Windows当中创建线程
createThread(NULL,0,thread_func, &client_socket);
存在问题因为client_socket在栈区,所以可能socket销毁而线程都还没有启动(线程的运行是抢时间片的步骤,有可能线程在启动的时刻没抢过主线程,主线程已经把客户端socket销毁了)
所以我们要单独创建一个客户端socket,单独申请一个内存
服务端代码更改
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
//定义一个线程函数 当做线程启动入口
DWORD WINAPI thread_func(LPVOID lpThreadParameter)
{
//传入client_socket 解引用拿到后 释放地址
SOCKET client_socket = *(SOCKET*)lpThreadParameter;
free(lpThreadParameter);
while (1) {
//5.开始通讯(B/S)
//通过浏览器访问一个网站的时候
//浏览器会发送一个http请求 http是tcp的上层协议 所以写tcp是可以接收http的报文的
//接收发送的报文
char buffer[1024] = { 0 };
/*int recv(
SOCKET s, //客户端socket
char* buf, //接受的数据存到哪里
int len, //接受的长度
int flags //0
);*/
int ret = recv(client_socket, buffer, 1024, 0);
if (ret <= 0) break;
printf("%llu:%s\n",client_socket, buffer);
//回显 接收完之后立马发送回去
send(client_socket, buffer, (int)strlen(buffer), 0);
}
//断开连接 打印一下
printf("socket: %llu,disconnect.\n", client_socket);
//6.关闭连接
closesocket(client_socket);
return 0;
}
int main()
{
//windows上使用网络功能需要开始网络权限
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
//1.创建socket套接字
/*socket(
int af, //协议地址簇 ipv4/ipv6 对应 AF_INET/AF_INET6
int type, //类型 流式协议/帧式协议 对应 SOCK_STREAM/SOCK_DGRAM
int protocol //保护协议 tcp/udp不用填保护协议 直接填0
);*/
//此处是服务端socket,所以我们一般起名叫监听socket
SOCKET listen_socket = socket(AF_INET, SOCK_STREAM, 0);
//SOCKET是一个无符号的长整型,就是一个整数,可以直接打印出来
//未开启网络结果为-1 即无效的socket -> INVALID_SOCKET
if (INVALID_SOCKET == listen_socket) {
printf("create listen socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//2.给这个socket绑定一个端口号
//struct sockaddr_in {
// ADDRESS_FAMILY sin_family; //协议地址簇
// USHORT sin_port; //端口号
// IN_ADDR sin_addr; //IP地址
// CHAR sin_zero[8]; //保留字节 -> 协议升级会用
//};
//大小端问题:
// 一个数字在计算机中存储以二进制存储,对端口来讲占两个字节
// 编程中最小单位我们通常用字节来算,很少用位算
// 存数据的时候会有先后的问题 8080 -> 1F90(大端序号,高位放前面【千百个十】)
// 但是本地电脑的存储方式是以小端序存储的【个十百千】
//unsigned short a = 8080;
//unsigned short* p = &a;
struct sockaddr_in local = { 0 };
local.sin_family = AF_INET;
local.sin_port = htons(8080); //大小端问题 中间设备使用的是大端序
//服务端 选项 网卡 127.0.0.1(本地环回)只接受哪个网卡的数据
//一般写0.0.0.0 不管哪个数据库来 只要有我都接受
//INADDR_ANY整数 占4个字节
//local.sin_addr.s_addr = htonl(INADDR_ANY);
//手动指定
local.sin_addr.s_addr = inet_addr("0.0.0.0"); //字符串IP转换成整数IP
//绑定 给上面定义的socket绑定我们指定的内容
/*int bind(
SOCKET s, //对哪个socket进行绑定
const struct sockaddr FAR * name, //sockaddr的结构
int namelen //长度
); 返回整数类型*/
//为什么使用sockaddr结构而不是sockaddr_in结构?(两个结构体的大小一模一样)
/*struct sockaddr {
ADDRESS_FAMILY sa_family;
CHAR sa_data[14];
} 用这个方便扩展 通用的结构*/
if (-1 == bind(listen_socket, (struct sockaddr*)&local, sizeof(local))) {
printf("bind socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//3.给socket开启监听属性
/*int listen(
SOCKET s,
int backlog //半连接队列长度
);*/
if (-1 == listen(listen_socket, 10)) {
printf("start listen socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
//以上准备工作结束
//4.等待客户端连接
//返回的客户端socket才是跟客户端可以通讯的一个socket
//listen_socket唯一的作用就是等待连接 最后返回一个socket出来
//accept()是阻塞函数,等到有客户端连接进来就接受连接,然后染回,否则就阻塞
/*SOCKET accept(
SOCKET s, //监听socket
struct sockaddr * addr, //客户端的IP地址和端口号
int * addrlen //结构的大小 为什么是指针 因为可填可不填 要填就要和上面的addr都填
);*/
//如果要跟多个客户端通讯 需要有while循环
while (1)
{
SOCKET client_socket = accept(listen_socket, NULL, NULL);
if (INVALID_SOCKET == client_socket)
continue;
//有新的连接产生 打印一下
printf("new connnet, socket: %llu\n", client_socket);
//单独申请一个客户端
SOCKET* sockfd = (SOCKET*)malloc(sizeof(SOCKET));
*sockfd = client_socket;
//连接一旦完成进入线程
//Windows当中创建线程
CreateThread(NULL,0,thread_func, sockfd, 0, NULL);
}
return 0;
}
代码实现
二.UDP
1.TCP与UDP服务端的不同之处
-
Socket类型:
- TCP使用
SOCK_STREAM
类型,而UDP使用SOCK_DGRAM
类型。
- TCP使用
-
连接方式:
- TCP是面向连接的,需要通过
connect
、listen
和accept
函数建立连接。 - UDP是无连接的,不需要建立连接,直接使用
sendto
和recvfrom
函数发送和接收数据。
- TCP是面向连接的,需要通过
-
数据传输:
- TCP提供可靠的、有序的数据传输,保证数据完整性。
- UDP提供不可靠的、无序的数据传输,可能丢包,但速度更快。
-
并发处理:
- TCP服务端通常需要为每个客户端连接创建一个线程或使用非阻塞I/O来处理并发。
- UDP服务端通常在主循环中使用
recvfrom
和sendto
处理所有客户端的数据,不需要为每个客户端创建线程。 - (UDP服务端示例代码中没有使用线程,因为UDP是无连接的,通常不需要为每个客户端创建线程。如果需要处理大量并发UDP客户端,可以考虑使用线程池或非阻塞I/O。)
-
错误处理:
- TCP在建立连接时会进行错误处理,如连接失败会返回错误。
- UDP在发送和接收数据时可能会遇到错误,如目标不可达等,需要在发送和接收时处理。
-
资源消耗:
- TCP由于需要维护连接状态,资源消耗相对较高。
- UDP不需要维护连接状态,资源消耗相对较低。
-
适用场景:
- TCP适用于需要可靠传输的场景,如文件传输、网页浏览等。
- UDP适用于对实时性要求高的场景,如视频会议、在线游戏等。
2.服务端代码
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include<stdio.h>
#include<string.h>
#include<WinSock2.h>
#pragma comment(lib,"ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET udp_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == udp_socket) {
printf("create UDP socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
struct sockaddr_in local = { 0 };
local.sin_family = AF_INET;
local.sin_port = htons(8080);
local.sin_addr.s_addr = inet_addr("0.0.0.0");
if (-1 == bind(udp_socket, (struct sockaddr*)&local, sizeof(local))) {
printf("bind socket failed !!! errocode: %d\n", GetLastError());
return -1;
}
printf("UDP server is running\n");
while (1) {
char buffer[1024] = { 0 };
struct sockaddr_in client_addr;
int addr_len = sizeof(client_addr);
int ret = recvfrom(udp_socket, buffer, 1024, 0, (struct sockaddr*)&client_addr, &addr_len);
if (ret > 0) {
printf("Received from %s:%d: %s\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port), buffer);
sendto(udp_socket, buffer, ret, 0, (struct sockaddr*)&client_addr, addr_len); // Echo back
}
}
closesocket(udp_socket);
WSACleanup();
return 0;
}
3.客户端代码
#define _CRT_SECURE_NO_WARNINGS 1
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
// 1. 创建socket套接字(通过套接字连接到服务端)
SOCKET client_socket = socket(AF_INET, SOCK_DGRAM, 0);
if (INVALID_SOCKET == client_socket) {
printf("create socket failed!!!\n");
return -1;
}
// 2. 定义服务器地址
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8080);
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 3. 开始通讯 send recv
while (1)
{
char sbuffer[1024] = { 0 };
printf("please enter: ");
scanf("%s", sbuffer);
// 发送数据到服务器
sendto(client_socket, sbuffer, strlen(sbuffer), 0, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 接收服务器回显
char rbuffer[1024] = { 0 };
int addr_len = sizeof(server_addr);
int ret = recvfrom(client_socket, rbuffer, 1024, 0, (struct sockaddr*)&server_addr, &addr_len);
if (ret <= 0) break;
rbuffer[ret] = '\0'; // 添加字符串结束符
printf("Received from server: %s\n", rbuffer);
}
// 4. 关闭socket
closesocket(client_socket);
WSACleanup(); // 释放 Winsock 资源
return 0;
}