-
2021-08-21 11:58:41
1. 套接字
套接字是由操作系统提供的网络数据通信软件设备,即使对网络数据传输原理不了解,也能够使用套接字完成网络数据传输。为了与远程计算机进行数据传输,需要连接到英特网,套接字就是进行网络连接的工具。
服务端:接收连接请求的套接字创建流程如下:
1. 调用socket函数创建套接字
2. 调用bind函数分配IP地址和端口号(port)
3. 调用listen函数,套接字转为可接受请求状态
4. 调用accept函数接收连接请求
在Linux系统中,一切皆文件,因此Linux中的socket也是一种文件,因此在数据的传输过程中,可以使用文件IO相关的函数进行操作。而在Windows系统中,文件和socket是有区别的。
2. 套接字的类型:
面向连接的套接字(SOCKET_STREAM)
如果socket函数的第二个参数为SOCKET_STREAM,则会创建面向连接的的套接字,即TCP套接字。面向连接的套接字(TCP套接字)传输数据的方式与传送到传输物品类似,即只要传送带本身没有问题,就不会导致数据的丢失。且较晚传输的数据不会先到达,保证了数据的按序传递,TCP套接字的传输方式具有如下特点:
1. 传输过程中数据不会丢失
2. 数据按序传输
3. 传输的数据不存在数据边界
在收发数据的套接字内部有缓冲区,通过套接字传输的数据 将保存在缓冲区中,套接字收到数据并不意味着马上调用read函数。只要不超过数组容量,则有可能在数据填充满缓冲区之后通过调用1次read函数读取全部数据。也有可能通过多次调用read函数读取所有数据。也就是说在TCP套接字中,调用read,write方法的次数并无太大意义,所以说TCP套接字不存在数据边界。
如果接收的套接字接受速度较慢,导致接收缓冲区被填满,此时发送的套接字便会停止数据发送,直到接收端调用read函数读取数据使得缓冲区中有空余位置时,发送端套接字才会继续接着发送,因此不会造成数据丢失,而且在传输过程中如果发生数据丢失,还会进行数据的重新传输。因此TCP套接字除特殊情况外不会发生数据丢失。
面向消息的套接字(SOCKET_DGRAM)
如果socket函数的第二个参数为SOCKET_DGRAM,则会创建面向消息的套接字,即UDP套接字。UDP套接字传输数据类似于高速移动的摩托快递。其传输方式具有如下的特点:
1.强调快速传输而非顺序传输 (不一定保证次序)
2.传输的数据可能丢失,也可能损毁,没有数据重传机制
3. 传输的数据有边界
4. 限制每次传输的数据的大小
类似于两件包裹发送至统一目的地,只要以最快的速度交给用户即可。在输送过程中无需保证包裹的次序。包裹有大小限制且在传送的过程中存在损毁或者丢失的风险。且如果分多次发送包裹,接收者也需要分多次进行接收,即“传输的数据具有边界“,因此UDP套接字是一种”不可靠的,不按序传递的,以数据的高速传输为目的的套接字“。
-----------------------------------------分割线--------------------------------------------
简单的服务端程序:《TCP/IP网络编程》书籍中的例子做了修改
例子:基于windows的服务端/客户端简socket通信单实现
功能:客户端输入计算表达式,再通过将表达式组成消息报文,发送给服务端,由服务端计算表达式的值,计算完成后再将结果返回给客户端,客户端对结果进行相应的展示:
消息报文格式如下所示:
索引 值 含义 0 C 消息头 1 K 2 0 数据长度(第四个字节以后的数据长度) 3 0 4 0 参与运算的数字个数 5 0 6 0 第一个运算数 7 0 8 0 9 0 10 0 第 n 个运算数 11 0 12 0 13 0 14 + - * / 运算符 15 16 17 18 服务端代码实现:
server.cpp
/* 简易计算器服务端代码 */ #include "stdafx.h" #include <stdio.h> #include <iostream> #include <WinSock2.h> #pragma comment(lib, "Ws2_32.lib") #define BUFF_SIZE 100 // 定义接收数据缓冲区字节大小 #define MESSAGE_HEAD_SIZE 4 // 消息头大小 #define OPERAND_SIZE 4 // 运算数所占字节大小 #define RESULT_SIZE 4 // 计算结果所占的字节数 #define RESULT_OVERFLOW -999999 // 计算结果溢出 typedef unsigned short ushort; typedef INT32 int32; typedef INT16 int16; void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main() { WSADATA wsadata; SOCKET serverSocket, clientSocket; sockaddr_in serverAddr; ushort port = 30100; // 定义端口号 // 定义缓冲区 char buffer[BUFF_SIZE]; memset(buffer, 0, BUFF_SIZE); int32 result = 0; int recvLen = 0; // 接收长度 int recvCount = 0; // 接收数据计数 // 初始化socket库 if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) { error_handle("Failed to init wsadata"); } // 初始化服务端套接字 serverSocket = socket(PF_INET, SOCK_STREAM, 0); // 服务端地址绑定 memset(&serverAddr, 0, sizeof(serverAddr)); serverAddr.sin_family = AF_INET; serverAddr.sin_addr.s_addr = INADDR_ANY; serverAddr.sin_port = htons(port); int serverAddrSize = sizeof(serverAddr); // 绑定端口 if (bind(serverSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) { error_handle("Failed to bind socket"); } if (listen(serverSocket, 5) == SOCKET_ERROR) { error_handle("Falied to listen"); } while (true) { // 等待接收连接 printf("Waiting for connction from client!\n"); clientSocket = accept(serverSocket, (sockaddr*)&serverAddr, &serverAddrSize); if (clientSocket == INVALID_SOCKET) { printf("Failed to get connect from client!\n"); continue; } printf("Successfully get connect from client!\n"); recvLen = recv(clientSocket, buffer, BUFF_SIZE, 0); // 先读取四个字节的数据 if (recvLen < 4) { // 数据包数据缺失 result = 0; // 记得result清零 memset(buffer, 0, BUFF_SIZE); // buffer记得清零 continue; // 重新等待接收连接 } // 校验消息头 if (buffer[0] != 'C' || buffer[1] != 'K') { printf("The message header is wrong!\n"); result = 0; // 记得result清零 memset(buffer, 0, BUFF_SIZE); // buffer记得清零 continue; } // 解析数据长度 int dataLen = buffer[2] | (buffer[3] << 8); // recvLen = MESSAGE_HEAD_SIZE; while (recvLen < (dataLen + 2 + 2)) { recvCount = recv(clientSocket, &buffer[recvLen], BUFF_SIZE-1, 0); // 计算实际接收的数据的个数 recvLen += recvCount; } printf("Successfully recv messgae.\n"); int operand_count = buffer[MESSAGE_HEAD_SIZE] | (buffer[MESSAGE_HEAD_SIZE+1] << 8); // 运算数的数量 char caloperator = buffer[MESSAGE_HEAD_SIZE + 2 + operand_count*OPERAND_SIZE]; if (caloperator != '+' && caloperator != '-' && caloperator != '*' && caloperator != '/') { // error_handle("Operator is invalid!"); printf("Operator %c is invalid!", caloperator); result = 0; // 记得result清零 memset(buffer, 0, BUFF_SIZE); // buffer记得清零 continue; } for (int i=0; i<operand_count; ++i) { int32 operand = *(int32*)&buffer[MESSAGE_HEAD_SIZE + 2 + i * OPERAND_SIZE]; if (i == 0) { result = operand; continue; } if (caloperator == '+') { result += operand; } else if (caloperator == '-') { result -= operand; } else if (caloperator == '*') { result *= operand; } else if (caloperator == '/') { if (operand == 0) { printf("The reuslt is overflow because the number is divided by zero"); result = RESULT_OVERFLOW; break; } result /= operand; } } // 返回结果 send(clientSocket, (char*)&result, RESULT_SIZE, 0); closesocket(clientSocket); result = 0; // 记得result清零 memset(buffer, 0, BUFF_SIZE); // buffer记得清零 } WSACleanup(); system("pause"); return 0; } // 服务端/客户端通信的消息格式 /* xx xx xx xxxx xxxx xxxx x 标识符 数据长度 运算数的个数(2bytes) 操作数1 操作数2 操作数n 运算符号 */
客户端代码:
client.cpp/* 建议服务器客户端代码 */ #include "stdafx.h" #include <iostream> #include <stdlib.h> #include <WinSock2.h> using std::cout; using std::endl; // 类型定义 typedef unsigned short ushort; typedef unsigned char uchar; typedef INT32 int32; typedef INT16 int16; #define BUFFER_SIZE 100 // 定义缓冲区字节大小 #define OPERAND_SIZE 4 // 定义操作数的所占字节 #define OPERATOR_SIZE 2 // 定义操作符所占字节的大小 #define RESULT_SIZE 4 // 返回结果所占的字节数 #define RESULT_OVERFLOW -999999 // 计算结果溢出 #pragma comment(lib, "Ws2_32.lib") //#define _WINSOCK_DEPRECATED_NO_WARNINGS //#define _CRT_SECURE_NO_WARNINGS void error_handle(char* message) { printf("%s\n", message); system("pause"); exit(1); } int main(int argc, char* argv[]) { printf("Starting the calculate client...\n"); // 定义数据区 char buffer[BUFFER_SIZE]; memset(buffer, 0, sizeof(buffer)); WSADATA wsadata; SOCKET hsocket; SOCKADDR_IN servAddr; // 服务器端地址 // 服务端的地址和端口号 char ipAddr[] = "127.0.0.1"; ushort port = 30100; if (WSAStartup(MAKEWORD(2, 2), &wsadata) != 0) // 返回0表示初始化成功 { error_handle("Failed to init socket lib."); // 初始化套接字相关的库失败 } hsocket = socket(PF_INET, SOCK_STREAM, 0); if (hsocket == INVALID_SOCKET) { error_handle("Failed to create socket"); } // 设置服务器地址以及端口 memset(&servAddr, 0, sizeof(servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = inet_addr(ipAddr); // inet_addr将字符串IP地址转成整数,且转成网络字节序 servAddr.sin_port = htons(port); // 连接服务器 if (connect(hsocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR ) { error_handle("Failed to connect to server\n"); } else { printf("Successfully connected to server %s: %d\n", ipAddr, port); } ushort operand_count = 0; printf("Plaese input operand count: "); scanf("%d", &operand_count); // 填充消息头 buffer[0] = 'C'; buffer[1] = 'K'; buffer[2] = 0; buffer[3] = 0; // 填充运算数个数 buffer[4] = (char) operand_count & 0x00ff; buffer[5] = (char) operand_count & 0xff00; for (int i=0; i<operand_count; ++i) { printf("Please input operand %d: ", i + 1); scanf("%d", (int32*)&buffer[i*OPERAND_SIZE + 6]); // 操作数占用四个字节 } // 填充运算符 printf("Please Input operator: "); scanf(" %c", &buffer[operand_count*OPERAND_SIZE + 6]); // 这里的%c前面必须加入空格,否则会因为前面输入按下的空格,而导致这里将前面按下的空格直接读入,导致输入值错误 // 填充数据长度 int dataLen = (operand_count * OPERAND_SIZE) + 2 + 1; // 前四个字节不计入数据长度 buffer[2] = (dataLen & 0xff); buffer[3] = (dataLen >> 8) & 0xff; // 发送数据包 send(hsocket, buffer, sizeof(buffer), 0); // 接收服务端的数据 int result; recv(hsocket, (char*)&result, sizeof(result), 0); if (result != RESULT_OVERFLOW) { printf("The calculate result is %d.\n", result); } else { printf("The calculate result is overflow!"); } closesocket(hsocket); WSACleanup(); // 避免控制台不出现 system("pause"); return 0; } // 服务端/客户端通信的消息格式 /* xx xx xx xxxx xxxx xxxx x 标识符 数据长度 运算数的个数(2bytes) 操作数1 操作数2 操作数n 运算符号 */
运行结果如下图所示:
客户端输入以及结果展示
服务端输出:
更多相关内容 -
tcp/ip网络编程参考代码
2020-12-04 14:57:40基于linux平台的tcp/ip网络编程参考代码,包括server端和client端代码,支持linux pc、嵌入式linux以及android(底层),支持gcc和ndk编译,已经验证通过,欢迎下载 -
windows TCP/IP 网络编程(七)5种windows网络模型(5)完成端口
2022-04-12 15:09:02windows TCP/IP 网络编程(七)5种windows网络模型(5)完成端口 -
TCP/IP网络编程 带书签目录 完整版.zip
2019-05-30 09:17:34本书以通俗易懂的语言详细介绍了TCP/IP及其工作原理,以简单明了的编程实例全面介绍了基于Winsock的网络程序设计技术 -
windows TCP/IP 网络编程(五)5种windows网络模型(4) 重叠IO模型(a)事件通知 DEMO
2022-04-11 23:13:38windows TCP/IP 网络编程(五)5种windows网络模型(4) 重叠IO模型(a)事件通知 DEMO -
TCP/IP 网络编程
2022-04-19 11:07:38TCP/IP 网络编程 一,查看源代码 服务端 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <netdb.h> #include <unistd.h> #...TCP/IP 网络编程
一,查看源代码
服务端
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <netdb.h> #include <unistd.h> #include <errno.h> #include <signal.h> #include <sys/wait.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> #include <stdlib.h> #include <ctype.h> //TODO: 某些头文件 #define MAX_BUFFER 1024 void* thread_function(void* arg_array) { int nr_bytes_read, char_index; char receive_and_send_buffer[MAX_BUFFER],*p; int* p_arg = (int*)arg_array; int thread_session_socket = *p_arg; printf("thread_session_socket = %d\n", thread_session_socket); while (1) { nr_bytes_read = read(thread_session_socket, receive_and_send_buffer, 1024);//TODO: 尝试从socket读取 if(nr_bytes_read == -1)//TODO: 如果错误,或没能从socket读取字符 break; printf("Message from client(%d): %s\n", nr_bytes_read, receive_and_send_buffer); for (p = receive_and_send_buffer; *p != '\0'; p++) *p = toupper(*p); //TODO: 转换为大写 write(thread_session_socket, receive_and_send_buffer,1024); //TODO: 返回给客户端 } close(thread_session_socket);//TODO: 关闭socket return 0; } int main(int argc, char* argv[]) { socklen_t size_of_client_sockaddr; pthread_t tid; int listen_socket; int session_socket; int return_code; int port_number; struct sockaddr_in client_remote_sockaddr; struct sockaddr_in server_local_sockaddr; //服务器端运行时要给出端口信息,该端口为监听端口 if (argc != 2) { printf("Usage:%s port_number \n", argv[0]); return 1; } //获得输入的端口 port_number = atoi(argv[1]); //创建套接字用于服务器的监听 listen_socket = socket(AF_INET, SOCK_STREAM, 0);//TODO: socket() if (listen_socket==-1) {//TODO: 如果出错 perror("ERR socket."); return 1; } //填充关于服务器的套节字信息 memset(&server_local_sockaddr, 0, sizeof(server_local_sockaddr)); server_local_sockaddr.sin_family = AF_INET; server_local_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY); server_local_sockaddr.sin_port = htons(port_number); //将服务器和套节字绑定 return_code = bind(listen_socket, (struct sockaddr *)&server_local_sockaddr,sizeof(server_local_sockaddr));//TODO: 调用bind绑定地址和端口(提供服务的位置) if (return_code == -1) {//TODO: 如果出错 perror("ERR bind."); close(listen_socket);//TODO: 关闭监听socket return 1; } //监听指定端口,连接5个客户端 return_code = listen(listen_socket,5);//TODO: 请求监听、提供服务 if (return_code == -1) {//TODO: 如果出错 perror("ERR listen."); close(listen_socket);//TODO: 关闭监听socket return 1; } //对每个连接来的客户端创建一个线程,单独与其进行通信。 //首先调用read函数读取客户端发送来的信息,将其转换成大写后发送回客户端,#退出。 while (1) { size_of_client_sockaddr = sizeof(client_remote_sockaddr); //TODO: 12345 改为自己的学号,此处不要修改write调用和STDOUT_FILENO参数! write(STDOUT_FILENO, "Listening & Accepting for 201930310132 ...\n", strlen("Listening & Accepting for 201930310132 ...\n")); session_socket = accept(listen_socket, (struct sockaddr *)&client_remote_sockaddr,&size_of_client_sockaddr);//TODO: 调用accept阻塞,接到客户机时返回 session_socket if (session_socket == -1) {//TODO: 如果出错 if (errno == EINTR) continue; else { perror("ERR accept(): cannot accept client connect request"); close(listen_socket);//TODO: 关闭socket return 1; } } printf("session_socket = %d\n", session_socket); //打印建立连接的客户端产生的套节字 return_code = pthread_create(&tid,NULL,thread_function,(void*)&session_socket);//TODO: 调用 pthread_create,将 session_socket传递给 thread_function if (return_code != 0) {//TODO: 如果出错 perror("ERR pthread_create()"); close(listen_socket);//TODO: 关闭一个socket close(session_socket);//TODO: 关闭另一个socket return 1; } } return 0; }
客户端
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <sys/un.h> #include <netdb.h> #include <unistd.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdlib.h> //TODO: 某些头文件 #define MAX_BUFFER 1024 int main(int argc, char* argv[]) { int local_socket; int return_code; char send_and_receive_buffer[MAX_BUFFER]; int port_number; int nr_bytes_read; static struct sockaddr_in server_sockaddr; //客户端运行需要给出具体的连接地址和端口 if (argc != 3) { printf("Usage: %s server_ip_address port_number \n", argv[0]); return 1; } //获得输入的端口 port_number = atoi(argv[2]); //创建套节字用于客户端的连接 local_socket = socket(AF_INET, SOCK_STREAM, 0);//TODO: 调用socket if (local_socket == -1) {//TODO: 如果错误 perror("ERR socket."); return 1; } //填充关于服务器的套节字信息 memset(&server_sockaddr, 0, sizeof(server_sockaddr)); server_sockaddr.sin_family = AF_INET; server_sockaddr.sin_addr.s_addr = inet_addr(argv[1]); server_sockaddr.sin_port = htons(port_number); //连接指定的服务器 return_code = connect(local_socket, (struct sockaddr*)&server_sockaddr, sizeof(server_sockaddr));//TODO: 调用connect if (return_code == -1) {//TODO: 如果错误 perror("ERR connect."); close(local_socket); return 1; } memset(send_and_receive_buffer, 0, MAX_BUFFER); //用户输入信息后,程序将输入的信息通过套接字发送给服务器 ,然后调用read函数从服务器中读取发送来的信息。 //输入“#”退出 while (1) { //TODO: 12345 改为自己的学号,此处不要修改write调用和STDOUT_FILENO参数! write(STDOUT_FILENO, "Type in a string for 201930310132:", strlen("Type in a string for 201930310132:")); nr_bytes_read = read(STDIN_FILENO,send_and_receive_buffer,1024);//TODO: 调用read, 从STDIN_FILENO读 if (nr_bytes_read > 0) write(local_socket, send_and_receive_buffer,1024);//TODO: 调用write,写socket nr_bytes_read = read(local_socket, send_and_receive_buffer,1024);//TODO: 调用read,读socket if (nr_bytes_read > 0) printf("Message form server: %s\n", send_and_receive_buffer); if (send_and_receive_buffer[0] == '#') break; } close(local_socket); return 0; }
简而言之,创建一个基本的socket还是比较简单的,过程可能有些繁琐。可以归纳为以下几步。
第一步:调用socket创建一个socket
第二步:调用bind函数绑定IP地址和端口号,绑定到操作系统
第三步:调用listen开始转为可接收请求状态
第四步:调用accept函数受理连接请求
客户端需要一个connect函数来连接到服务器
利用write和read进行数据的读写操作
关闭socket二,编译并运行
三,多线程模式
再开启一个客户端,服务端会对每个连接来的客户端创建一个线程,单独与其进行通信。
参考文章
https://blog.csdn.net/u014634338/article/details/48551755
https://blog.csdn.net/yanchuang1/article/details/48049259
https://blog.csdn.net/u011675745/article/details/78555250 -
TCP/IP网络编程---Linux系统下的TCP套接字编程
2022-02-21 08:39:25第一章 理解网络编程和套接字 1.1 网络编程和套接字概要 1.2 基于Linux的文件操作 1.2.1 底层文件访问和文件描述符 1.2.2 打开文件 1.2.3 关闭文件 1.2.4 将数据写入文件 1.2.5 读取文件中的数据 第二章 套...目录
第一章 理解网络编程和套接字
1.1 网络编程和套接字概要
网络编程就是编写程序使两台连网的计算机相互交换数据。首先需要的是物理连接,目前大部分计算机都连接到庞大的互联网中;在此基础上,只需考虑如何编写数据传输软件,而操作系统为我们提供了名为"套接字"的部件,套接字是数据网络传输用的软件设备。网络编程又称为套接字编程。
服务器端是能够受理连接请求的程序,服务器端创建的套接字称为服务器端套接字或监听套接字;客户端是用于请求连接的,客户端创建的套接字称为客户端套接字。
1.2 基于Linux的文件操作
在Linux系统中,套接字socket也被认为是文件的一种,因此在网络传输过程中可以使用文件I/O的相关函数;而Windows系统于Linux不同,要区分socket和文件,因此两种系统中的编程方式也不相同。之后主要学习的是Linux的编程方式。
1.2.1 底层文件访问和文件描述符
底层指的是与标准无关的操作系统独立提供的。文件描述符是系统分配给文件或套接字的整数,这个整数将成为程序员与操作系统之间良好沟通的渠道,是为了方便称呼操作系统创建的文件或套接字而赋予的数。有一些文件描述符是固定的,比如C语言中学习的标准输入输出及标准错误,即描述符从3开始由小到大的顺序编号,因为0、1、2是分配给标准I/O的描述符。
文件描述符 对象 0 标准输入:Standard Input 1 标准输出:Standard Output 2 标准错误:Standard Error 文件和套接字一般经过创建过程才会被分配文件描述符。
1.2.2 打开文件
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *path, int flag);//成功时返回文件描述符,失败时返回-1
- path:文件名的字符串地址
- flag:文件打开模式信息
文件打开模式flag可能的常量值和含义:
打开模式 含义 O_CREAT 必要时创建文件 O_TRUNG 删除全部现有数据 O_APPEND 维持现有数据,保存到其后面 O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWD 读写打开 1.2.3 关闭文件
#include <unistd.h>
int close(int fd);//成功时返回0,失败时返回-1
- fd:需关闭的文件或套接字的文件描述符
此函数不仅可以关闭文件,也可以关闭套接字。
1.2.4 将数据写入文件
#include <unistd.h>
ssize_t write(int fd,const void *buf,size_t nbytes);//成功时返回写入的字节数,失败时返回-1
- ssize_t:signed int size_t:unsigned int 使用typedef声明的两个数据类型
- fd:显示数据传输对象的文件描述符
- buf:保存要传输数据的缓存地址值
- nbytes:要传输数据的字节数
用于向文件输出(传输数据);通过套接字向其他计算机传递数据。
1.2.5 读取文件中的数据
#include <unistd.h>
ssize_t read(int fd, void *buf,size_t nbytes);
- fd:显示数据传输对象的文件描述符
- buf:保存要接收数据的缓存地址值
- nbytes:要接收数据的最大字节数
第二章 套接字类型与协议设置
2.1 套接字协议及其数据传输特性
2.1.1 关于协议
协议是对话中使用的通信规则,在计算机领域内理解为计算机间对话必备通信规则。
2.1.2 创建套接字
#include <sys/socket.h>
int socket(int domain, int type, int protocol);//成功时返回文件描述符,失败时返回-1
- domain:套接字中使用的协议族信息
- type:套接字数据传输类型信息
- protocol:计算机间通信中使用的协议信息
头文件sys/socket.h中声明的协议族:
名称 协议族 PF_INET IPv4互联网协议族 PF_INET6 IPv6互联网协议族 PF_LOCAL 本地通信的UNIX协议族 PF_PACKET 底层套接字的协议族 PF_IPX IPX Novell协议族 套接字类型Type指的是套接字的数据传输方式,因为决定了协议族并不能同时决定数据传输方式,一个协议族内存在多种数据传输方式:
- SOCK_STREAM:面向连接的套接字。
- 特征:
- 传输过程中数据不会消失
- 按序传输数据
- 传输的数据不存在数据边界
- 传输的数据不存在数据边界,指的是收发数据的套接字内部有缓冲(字节数组),因此收到数据并不意味着马上调用read函数,只要不超过缓冲容量,则有可能在数据填充满缓冲后1次read函数调用读取全部,也有可能分成多次read函数调用进行读取。在面向连接的套接字中,read函数和write函数的调用次数并无太大意义
- 面向连接的套接字连接必须一一对应,只能与另外一个同样特性的套接字连接
- 可靠的、按序传递的、基于字节的面向连接的数据传输方式的套接字
- 特征:
- SOCK_DGRAM:面向消息的套接字。
- 特征:
- 强调快速传输而非传输顺序
- 传输的数据可能丢失也可能损毁
- 传输的数据有数据边界
- 限制每次传输的数据大小
- 接收数据的次数应和传输次数相同
- 不可靠的、不按序传递的、以数据的高速传输为目的的套接字
- 特征:
第三个参数决定最终采用的协议。除非遇到同一协议族中存在多个数据传输方式相同的协议,大部分情况下可以向第三个参数传递0。
常用调用方式:
int tcp_socket = socket(PF_INET, SOCK_STREAM, 0);
0指的是满足PF_INET和SOCK_STREAM条件的协议IPPROTO_TCP
int udp_socket = socket(PF_INET, SOCK_DGRAM, 0);
0指的是满足PF_INET和SOCK_DGRAM条件的协议IPPROTO_UDP
第三章 地址族与数据序列
3.1 分配给套接字的IP地址与端口号
IP(Internet Protocol),是为收发网络数据而分配给计算机的值。端口号是为区分程序中窗口的套接字而分配给套接字的符号。
3.1.1 网络地址
IP地址分为两类:
IPv4:4字节地址族
IPv6:16字节地址族
IPv4标准的4字节IP地址分为网络地址和主机(指计算机)地址,分为A、B、C、D、E类型。数据传输时先根据网络地址找到计算机所属的局域网,再根据主机地址找到该局域网下的那个计算机。向相应网络传输数据实际上是向构成网络的路由器或交换机传递数据,由接收数据的路由器根据数据中的主机地址向目标主机传递数据。
通过IP地址的第一个字节判断网络地址占用的字节数,即判断所属类型:
- A类地址的首字节范围:0~127
- B类地址的首字节范围:128~191
- C类地址的首字节范围:192~223
3.1.2 用于区分套接字的端口号
IP用于区分计算机,只要有IP地址就能向目标主机传输数据,但仅凭这些无法传输给最终的应用程序。计算机中一般配有NIC(网络接口卡)数据传输设备,通过NIC向计算机内部传输数据时会用到IP。操作系统负责把传递到内部的数据适当分配给套接字,这是就要利用端口号。即通过NIC接收的数据内有端口号,操作系统正是参考此端口号把数据传输给相应端口的套接字。
端口号就是在同一操作系统内为区分不同套接字而设置的,不能将1个端口号分配给不同的套接字。端口号由16位构成,可分配的端口号范围是0-65535。注意:虽然端口号不能重复,但TCP套接字和UDP套接字不会共用端口号,所以允许重复。
3.2 地址信息的表示
3.2.1 表示IPv4地址的结构体
struct sockaddr_in{
sa_family_t sin_family; //地址族
uint16_t sin_port; //16位TCP/UDP端口号
struct in_addr sin_addr; //32位ip地址
char sin_zero[8]; //不使用
}
struct in_addr{
In_addr_t s_addr; //32位IPv4地址
}
这些数据类型参考:
- 成员sin_family:每种协议族适用的地址族均不同。
地址族 含义 AF_INET IPv4网络协议中使用的地址族 AF_INET6 IPv6网络协议中使用的地址族 AF_LOCAL 本地通讯中采用的UNIX协议的地址族 - 成员sin_port:保存16位端口号,重点在于以网络字节序保存。
- 成员sin_addr:保存32位IP地址信息,也以网络字节序保存。
- 成员sin_zero:无特殊含义,只是为使结构体sockaddr_in的大小与sockaddr结构体保持一致而插入的成员,必须填充为0。
struct sockaddr{
sa_family_t sin_family; //地址族
char sa_data[14]; //地址信息
}
- sa_data保存的地址信息中需包含IP地址和端口号,剩余部分应应填充0。
3.3 网络字节序与地址变换
3.3.1 字节序与网络字节序
CPU向内存保存数据的方式有两种:
- 大端序:高位字节存放在低位地址
- 小端序:高位字节存放在高位地址
代表CPU数据保存方式的主机字节序在不同CPU中也各不相同(目前主流的Intel系列CPU以小端序方式保存数据),因此如果两台计算机的CPU数据保存方式不同,会导致传送的数据和接收后解析的数据不相同。
正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序,其实非常简单,就是统一为大端序,先把数据数组转化成大端序格式再进行网络传输。
3.3.2 字节序转换
转换字节序的函数:
unsigned short htons(unsigned short);
unsigned short ntohs(unsigned short);
unsighed long htonl(unsigned long);
unsigned long ntohl(unsigned long);
- h代表主机host字节序
- n代表网络network字节序
- s代表short类型,用于端口号转换
- l代表long类型(Linux中long类型占用4个字节),用于IP地址转换
注意:数据的收发过程中有自动转换机制,因此除了向sockaddr_in结构体变量填充数据外,其他情况无需考虑字节序问题。
3.4 网络地址的初始化与分配
3.4.1 将字符串信息转换为网络字节序的整数型
sockaddr_in中保存地址信息的成员为32位整数型,因此需要把点分十进制表示法形式的IP地址转换成32位整数型数据。这个函数在转换类型的同时进行网络字节序转换。
#include <arpa/inet.h>
in_addr_t inet_addr(const char *string);//成功时返回32位大端序整数型值,失败时返回INADDR_NONE
int inet_aton(const char *string, struct in_addr *addr);//成功时返回1,失败时返回0
- string:含有需转换的IP地址信息的字符串地址值
- addr:将保存转换结果的in_addr结构体变量的地址值
调用inet_addr函数,需将转换后的IP地址信息带入sockaddr_in结构体中声明的in_addr结构体变量。而inet_aton函数不需要此过程,原因在于,若传递in_addr结构体变量值,函数会自动把结果传入该结构体变量。
#include <arpa/inet.h>
char* inet_ntoa(struct in_addr adr);//成功时返回转换的字符串地址值,失败时返回-1
此函数可以把网络字节序整数型IP地址转换为字符串形式。这个函数在内部申请了内存并保存了字符串,因此调用完该函数后,应立即将字符串信息复制到其他内存空间,防止再次调用该函数产生的覆盖问题。
3.4.2 网络地址初始化
常见的网络地址信息初始化方法:
3.4.3 INADDR_ANY
利用常数INADDR_ANY分配服务器的IP地址,采用这种方法,可以以自动获取运行服务器端的计算机IP地址。
3.4.4 向套接字分配网络地址
#include <sys/socket.h>
int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);//成功时返回0,失败时返回-1
- sockfd:要分配地址信息(IP地址和端口号)的套接字文件描述符
- myaddr:存有服务端地址信息的结构体变量地址值
- addrlen:第二个结构体变量的长度
常见的套接字初始化过程:
第四章 基于TCP的服务器端
4.1 理解TCP和UDP
根据数据传输方式的不同,基于网络协议的套接字一般分为TCP套接字和UDP套接字。
TCP套接字是面向连接的,又称基于流的套接字。TCP是Transmission Control Protocol (传输控制协议),意为对数据传输过程的控制。
4.1.1 TCP/IP协议栈
IP本身是面向消息的、不可靠的协议,用于路径的选择,如果传输中发送路径错误,则选择其他路径,如果发生数据丢失或错误,则无法解决。TCP和UDP层以IP层提供的路径信息为基础完成实际的数据传输。其中TCP在数据交换过程中可以确认对方已收到数据,并重传丢失的数据,因此TCP协议确认后向不可靠的IP协议赋予可靠性。
根据程序特点决定服务器端和客户端之间的数据传输规则(规定),这就是应用层协议。
4.2 实现基于TCP的服务器端/客户端
4.2.1 TCP服务器端的默认函数调用顺序
4.2.2 服务端进入等待连接请求阶段
调用bind函数给套接字分配了地址,接下来通过调用listen函数进入等待连接请求状态。只有服务端调用了listen函数,客户端才能进入可发生连接请求的状态(此时客户端才能调用connect函数)。
#include <sys/socket.h>
int listen(int sock,int backlog);//成功时返回0,失败时返回-1
- sock:希望进入等待连接请求状态的套接字文件描述符,传递的描述符套接字参数成为服务器端套接字(监听套接字)
- backlog:连接请求等待队列的长度,若为5,则队列长度为5,表示最多使5个连接请求进入队列
客户端请求连接时,服务端受理连接前一直使请求处于等待状态。
4.2.3 服务端受理客户端连接请求
调用listen函数后,若有新的连接请求,则应按序受理。受理请求意味着进入可接收数据的状态。此时除了listen创建出的套接字外,还需要另外一个套接字,来连接到发起请求的客户端,接受数据并写回数据,但是这个套接字不需要自己手动创建,下面这个函数会自动创建套接字。
#include <sys/socket.h>
int accept(int sock, struct sockaddr *addr, socklen_t *addrlen);//成功时返回创建的套接字文件描述符,失败时返回-1
- sock:服务器套接字的文件描述符
- addr:保存发起请求的客户端地址信息的变量地址值,调用函数后向传递来的地址变量参数填充客户端地址信息
- addrlen:第二个参数addr结构体的长度,但是存有长度的变量地址。函数调用完后,该变量即被填入客户端地址长度
accept函数内部将产生用于数据I/O的套接字,并返回其文件描述符,需要强调的是套接字是自动创建的,并自动与发起连接请求的客户端建立连接。
4.2.4 TCP客户端的默认函数调用顺序
创建客户端套接字后向服务器端发起请求连接。
#include <sys/socket.h>
int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);//成功时返回0,失败时返回-1
- sock:客户端套接字文件描述符
- servaddr:保存目标服务器端地址信息的变量地址值
- addrlen:以字节为单位传递已传递给第二个结构体参数servaddr的地址变量长度
客户端调用connect函数后,发生以下情况之一才会返回(完成函数调用):
- 服务器端接收连接请求
- 注意:接收连接请求并不意味着服务器端调用accept函数,而是服务器端把连接请求信息记录到等待队列
- 发生断网等异常情况而中断连接请求
客户端套接字何时、何地、如何分配地址信息?
何时:调用connect函数时
何地:操作系统内核中
如何:IP用计算机(主机)的IP,端口随机
4.2.5 基于TCP的服务器端/客户端函数调用关系
注意:
- 客户端只能等到服务器调用listen函数后才能调用connect函数
- 客户端调用connect函数前,服务器端有可能率先调用accept函数(此时服务器端在调用accept函数时进入阻塞状态,直到客户端调用connect函数为止)
4.3 实现迭代服务器端/客户端
4.3.1 实现迭代服务器端
服务器端在同一时刻只能服务于一个客户端,即只能连接一个客户端,因此每一次都要重新调用accept函数。
- 但是可能存在一个问题:由于TCP是面向连接的套接字,不存在数据边界,客户端多次调用write函数传递的字符串有可能一次性传递到服务器端,此时客户端可能同时把这些字符串接收到,导致不满足我们的期望;也有可能客户端还没有收到全部数据包就调用了read函数。
- 解决方法:应用层协议的定义,收发数据时要根据期望要求定义好规则(协议)来表示数据的边界。
第五章 TCP原理
5.1 TCP套接字中的I/O缓冲
write函数调用后并非立即传输数据,read函数调用后也并非马上接收数据,而是write函数调用瞬间,数据将移至输出缓冲,read函数调用瞬间,从输入缓冲读取数据。
- I/O缓冲在每个TCP套接字中单独存在
- I/O缓冲中在创建套接字时自动生成
- 即使关闭套接字也会继续传递输出缓冲中遗留的数据
- 关闭套接字将丢失输入缓冲中的数据
TCP中有滑动窗口协议,会控制数据流(流量控制),不会发生超过输入缓冲大小的数据传输。因此TCP中不会因为缓冲溢出而丢失数据。
TCP套接字从创建到消失过程:
- 与对方套接字建立连接
- 与对方套接字进行数据交换
- 断开与对方套接字的连接
5.2 TCP内部工作原理1:与对方套接字的连接
TCP在实际通信过程中会经过三次对话过程,三次握手。
套接字是以双全工方式工作的,可以双向传递数据。
TCP连接收发数据前都会向数据包分配序号,并向对方通报此序号,防止数据丢失。
5.3 TCP内部工作原理2:与对方主机的数据交换
每一次ACK号的增量为传输的数据字节数。
5.4 TCP的内部工作原理3:断开与套接字的连接
断开连接时也需要双方协商,四次握手。
PS:参考书籍《TCP/IP网络编程》 尹圣雨著 归纳整理
-
计算机网络---TCP/IP网络编程实验
2021-12-12 15:15:41文章目录计算机网络---TCP/IP网络编程实验一、实验目的二、实验环境和任务三、实验步骤及实验结果记录四、实验结果分析和总结 计算机网络—TCP/IP网络编程实验 一、实验目的 掌握TCP/IP协议的基本知识,TCP/IP协议...计算机网络—TCP/IP网络编程实验
一、实验目的
-
掌握TCP/IP协议的基本知识,TCP/IP协议的基本工作原理。
-
理解Socket的基本概念和工作原理,掌握Socket建立、监听、连接、数据发送和接收的方法。
-
进一步掌握客户/服务器应用程序的设计方法,利用底层的Windows Sockets API函数实现简单的 Winsock 网络应用程序设计,提高Windows平台上的Socket编程能力。
二、实验环境和任务
-
设计一程序,利用UDP实现点对点聊天。
-
设计一程序,实现Web服务器的基本功能。
三、实验步骤及实验结果记录
1、实验步骤说明与截图
客户端
服务器端
2、实验结果说明与截图
实现点对点聊天
实现web服务器的基本功能
四、实验结果分析和总结
-
编写一个简单的局域网消息发送程序。
-
编写一个程序利用Winsock实现语音全双工通信。
-
编写一个程序实现浏览器功能。
-
编写一个FTP服务器程序设计。
-
编写一个FTP客户端程序。
-
编写一个邮件监视程序,监视邮件服务器上是否有新邮件到达。
-
比较阻塞方式与非阻塞方式时应用程序的性能。
-
试利用C++Builder中系统提供的相关组件来实现问题1~6程序。
-
-
Go语言中TCP/IP网络编程的深入讲解
2020-09-20 12:13:23主要给大家介绍了关于Go语言中TCP/IP网络编程的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧 -
【TCP/IP网络编程】(02):基于TCP的客户端/服务端
2022-03-15 21:29:05本系列内容根据《TCP/IP网络编程》撰写.根据此书总结一些入门网络编程所需的知识,理解这些内容可以更好地让我们进入到后续的高级网络编程中。(以理解知识点为主,而不侧重于完整的代码编写) -
TCP/IP网络编程项目式教程(微课版)
2019-11-24 10:50:52TCP/IP网络编程项目式教程(微课版)是由唐四薪编著、清华大学出版社于2019年11月出版的图书。 本书按照问题驱动、由浅入深的理念,以项目实例的形式介绍基于Visual C++的TCP/IP WinSock编程方法。 本书是微课版,... -
《TCP/IP网络编程》阅读笔记
2022-01-03 21:07:34阅读 ICP/IP网络编程 后做的笔记,本人只是阅读了一遍,当作参考书看,因此一部分内容笔记并不全面。有些具体内容标注了在书本的第几页 《TCP/IP网络编程》 -
《TCP/IP网络编程》尹圣雨 例程源码
2021-02-03 16:16:19《TCP/IP网络编程》尹圣雨 例程源码 -
windows TCP/IP 网络编程(七)5种windows网络模型(5)完成端口 思维导图
2022-04-12 11:15:59windows TCP/IP 网络编程(七)5种windows网络模型(5)完成端口 思维导图 -
windows TCP/IP 网络编程(一)基于tcp/ip协议的c/s模型
2022-03-30 23:52:43概念:tcp/ip协议族(簇, 组,体系),并不是tcp协议和ip协议的总称,tcp/ip指的是整个网络传输体系。而tcp协议和ip协议就是单单的两个协议 特点: tcp/ip:面向连接的、可靠的、基于字节流的传输层协议 udp/... -
windows TCP/IP 网络编程(六)5种windows网络模型(4) 重叠IO模型(b)完成例程 DEMO
2022-04-12 10:36:01windows TCP/IP 网络编程(六)5种windows网络模型(4) 重叠IO模型(b)完成例程 DEMO -
TCP_IP网络编程技术基础
2018-09-21 01:40:45主要内容包括TCP/IP网络通信原理、套接字API、循环的无连接的服务器例程剖析、循环的面向连接的服务器例程剖析、服务器进程中的并发机制、基于多进程的并发的面向连接服务器例程剖析、基于多线程的并发的面向连接... -
基于TCP/IP网络编程的java控制台聊天室项目
2017-12-28 13:26:31java开发的基于TCP/IP协议的网络编程聊天室项目,结合几乎所有Java高级特效的知识,主要用于局域网之间的无网络通信(插着网线不能上网那种)局限是只能发文字 -
《TCP/IP网络编程》课后练习答案第一部分6~10章 尹圣雨
2019-04-01 21:11:14UDP为什么比TCP速度快?为什么TCP数据传输可靠而UDP数据传输不可靠? UDP和TCP不同,不进行流量控制。由于该控制涉及到套接字的连接和结束,以及整个数据收发过程,因此,TCP传输的数据是可以信赖的。相反,UDP不进行... -
《TCP/IP网络编程》(韩 尹圣雨)学习系列1,学习笔记
2020-12-28 18:57:37第一部分 开始网络编程 Chapter 1 理解网络编程和套接字 1.1 理解网络变编程和套接字 一.构建接电话套接字 1.调用socket函数(安装电话机)时进行的对话 问:“接电话需要准备什么?” 答:“当然是电话机” #... -
《TCP IP网络编程》.pdf
2018-03-06 11:10:05为初学者准备的网络编程! 韩国TCP/IP经典教程!手把手教你套接字编程! 本书涵盖操作系统、系统编程、TCP/IP协议等多种内容,结构清晰、讲解细致、通俗易懂。书中收录丰富示例,详细展现了Linux和Windows平台下套... -
TCP/IP网络编程(Windows环境C++代码)实现
2019-10-06 13:39:08代码完全来自《TCP/IP网络编程》(韩 尹圣雨)这本书,我也刚开始学,代码细节完全不懂,只是敲了一遍,能跑通程序而已。 1. 环境:1台电脑(能上网)、Windows操作系统(我的是Win10)、Visual Studio(我的是VS... -
TCP/IP 网络编程 学习资料
2017-11-08 23:53:28很好的TCP/IP学习资料,PPT讲解配合源代码,深入浅出,要学习TCP/IP协议的可以下载看看。建议先看PPT资料 -
《TCP IP网络编程》
2015-06-02 22:23:10TCP/IP网络编程涵盖操作系统、系统编程、TCP/IP 协议等多种内容,结构清晰、讲解细致、通俗易懂。书中收录丰富示例,详细展现了Linux 和Windows 平台下套接字编程的共性与个性。特别是从代码角度说明了不同模型... -
TCP/IP网络编程基础知识
2018-05-26 10:33:511. Socket1)Socket简介a)一种编程接口,用于不同计算机之间通信的接口b)一种特殊的文件描述符c)并不局限于TCP/IP协议d)面向连接和面向无连接的Socket都存在e)独立于具体协议,TCP和UDP都可以使用2)Socket类型... -
net-lenrning-reference__TCP/IP网络编程笔记
2021-01-17 13:21:33TCP/IP网络编程笔记 -
TCP/IP高效编程: 改善网络程序的44个技巧 (高清带书签)
2018-07-17 17:00:01TCP/IP高效编程: 改善网络程序的44个技巧 (高清带书签) -
基于Linux的TCP_IP网络通信编程.pdf
2021-09-07 00:09:58基于Linux的TCP_IP网络通信编程.pdf -
TCP/IP与网络编程
2018-03-30 10:22:426.1 网络编程技术概述 6.2 Socket基础 6.3 基本Socket系统调用 6.4 TCP Socket编程模式 6.5 UDP Socket编程模式 6.6 名字与地址转换 -
TCP/IP网络编程之四书五经
2014-07-07 21:37:42TCP/IP网络编程之四书五经 孟岩 TCP/IP协议是目前广域网和局域网通用的网络协议,因此,基于TCP/IP的编程就格外重要。从应用