以往都是在嵌入式设备中经常操作串口,或者使用QT的串口类。在Win32中处理串口也是有办法的,操作文件的打开和读写进行串口的操作。使用ReadFile、WriteFile函数。

基本步骤

使用Win32文件方式操作:打开串口(创建文件)->配置参数->发送(写文件)-->接收(读文件)

打开串口

打开串口的第一步是初始化或设置串口配置,目的是创建串口代理,整篇文章我们都将用文件句柄作为串口代理。

  • 创建端口句柄
  • 获取配置(DCB)
  • 修改配置
  • 保存配置
  • 设置通讯超时

创建端口句柄
串口句柄是可以被用来存取的串口对象句柄,创建串口句柄的函数是CreateFile,如下代码所示:

handlePort_ = CreateFile(portName, // 端口设备: 默认 "COM1"
GENERIC_READ | GENERIC_WRITE, // 设备打开模式: 允许读写
0, // 不共享
NULL, // 默认安全设置
OPEN_EXISTING, // 打开方式:打开已经存在的端口
0, // 默认 NULL
); // 默认

这其中需要注意的是:portName是个LPCSTR类型,需要注意字符编码。直接用c++的m_portName.c_str()即可。若是UNICODE的字符串,则需要转换下wchar to char。

std::wstring ansi2Wchar(LPCSTR pszSrc, int nLen)
{int nSize = MultiByteToWideChar(CP_ACP, 0, (LPCSTR)pszSrc, nLen, 0, 0);if (nSize <= 0)return NULL;WCHAR* pwszDst = new WCHAR[nSize + 1];if (NULL == pwszDst)return NULL;MultiByteToWideChar(CP_ACP, 0, (LPCSTR)pszSrc, nLen, pwszDst, nSize);pwszDst[nSize] = 0;if (pwszDst[0] == 0xFEFF)  // skip 0xFEFFfor (int i = 0; i < nSize; i++)pwszDst[i] = pwszDst[i + 1];std::wstring wcharString(pwszDst);delete[] pwszDst;pwszDst = nullptr;return wcharString;
}

以下为打开串口的封装:

bool WindowsSerialPort::openCom()
{bool open = false;#ifdef _WIN32DCB dcb;SecureZeroMemory(&dcb, sizeof(DCB));auto comName = ansi2Wchar(m_portName.c_str(), m_portName.size());
#ifdef UNICODEwchar_t* buffer = new wchar_t[m_portName.size()];MultiByteToWideChar(CP_ACP, 0, m_portName.c_str(), m_portName.size(), buffer, m_portName.size() * sizeof(wchar_t));buffer[m_portName.size()] = 0;m_port = ::CreateFile(buffer, GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_NO_BUFFERING, NULL);delete[] buffer;buffer = NULL;
#elsem_port = ::CreateFile(m_portName.c_str(), GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);
#endifif (m_port == INVALID_HANDLE_VALUE){open = false;LOGGING_ERROR("%s is invalid", m_portName.c_str());m_port = INVALID_HANDLE_VALUE;return open;}if (!GetCommState(m_port, &dcb)){open = false;LOGGING_ERROR("%s cannot get status", m_portName.c_str());m_port = INVALID_HANDLE_VALUE;return open;}dcb.BaudRate = m_baud;dcb.ByteSize = 8;dcb.fParity = NOPARITY;dcb.Parity = NOPARITY;dcb.StopBits = 0;dcb.fInX = 0;dcb.fOutX = 0;dcb.fOutxCtsFlow = 0;dcb.fRtsControl = 0;auto err = SetCommState(m_port, &dcb);if (!err){open = false;LOGGING_ERROR("%s cannot set comm status", m_portName.c_str());m_port = INVALID_HANDLE_VALUE;return open;}COMMTIMEOUTS timeout;memset(&timeout, 0, sizeof(timeout));GetCommTimeouts(m_port, &timeout);// TODO: 可能需要根据实际情况调整该参数timeout.ReadTotalTimeoutConstant = 50;timeout.ReadIntervalTimeout = 10;SetCommTimeouts(m_port, &timeout);open = true;#endifreturn open;
}

COMMTIMEOUTS 含义

COMMTIMEOUTS主要用于串口超时参数设置。

COMMTIMEOUTS结构如下:

typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout;  //间隔超时
DWORD ReadTotalTimeoutMultiplier;
DWORD ReadTotalTimeoutConstant;
DWORD WriteTotalTimeoutMultiplier;
DWORD WriteTotalTimeoutConstant;
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;

间隔超时 =  ReadIntervalTimeout
总超时   =   ReadTotalTimeoutMultiplier   * 字节数   +   ReadTotalTimeoutConstant

// COMMTIMEOUTS对象 COMMTIMEOUTS comTimeOut;
// 接收时,两字符间最大的时延
comTimeOut.ReadIntervalTimeout = 3;
// 读取每字节的超时
comTimeOut.ReadTotalTimeoutMultiplier = 3;
// 读串口数据的固定超时 // 总超时 = ReadTotalTimeoutMultiplier * 字节数 ReadTotalTimeoutConstant
comTimeOut.ReadTotalTimeoutConstant = 2;
// 写每字节的超时
comTimeOut.WriteTotalTimeoutMultiplier = 3;
// 写串口数据的固定超时
comTimeOut.WriteTotalTimeoutConstant = 2;
// 将超时参数写入设备控制
SetCommTimeouts(handlePort_,&comTimeOut);

ReadIntervalTimeout
指定通讯线上两个字符到达的最大时延,以毫秒为单位。在ReadFile操作期间,时间周期从第一个字符接收到算起。如果收到的两个字符之间的间隔超过该值,ReadFile操作完毕并返回所有缓冲数据。如果ReadIntervalTimeout为0,则该值不起作用。

如果值为MAXDWORD, 并且ReadTotalTimeoutConstant和ReadTotalTimeoutMultiplier两个值都为0, 则指定读操作携带已经收到的字符立即返回,即使没有收到任何字符。

ReadTotalTimeoutMultiplier
指定以毫秒为单位的累积值。用于计算读操作时的超时总数。对于每次读操作,该值与所要读的字节数相乘。

ReadTotalTimeoutConstant
指定以毫秒为单位的常数。用于计算读操作时的超时总数。对于每次读操作,ReadTotalTimeoutMultiplier与所要读的字节数相乘后与该值相加。

如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都为0,则在读操作时忽略总超时数。

WriteTotalTimeoutMultiplier
指定以毫秒为单位的累积值。用于计算写操作时的超时总数。对于每次写操作,该值与所要写的字节数相乘。

WriteTotalTimeoutConstant
指定以毫秒为单位的常数。用于计算写操作时的超时总数。对于每次写操作, WriteTotalTimeoutMultiplier与所要写的字节数相乘后与该值相加。

如果 WriteTotalTimeoutMultiplier 和 WriteTotalTimeoutConstant都为0,则在写操作时忽略总超时数。
提示:用户设置通讯超时后,如没有出错,串口已经被打开。

串口数据读取过程

串口读取事件分为两个阶段:
第一个阶段:串口执行到ReadFile()函数时,串口还没有开始传输数据,所以串口缓冲区的第一个字节是没有装数据的,这时候总超时起作用,如果在总超时时间内没有进行串口数据的传输,ReadFile()函数就返回,当然 没有读取到任何数据。而且,间隔超时并没有起作用。

第二阶段:假设总超时为20秒,程序运行到ReadFile(),总超时开始从0 计时,如果在计时到达10秒时,串口开始了数据的传输。那么从接收的第一个字节开始,间隔超时就开始计时,假如间隔超时为1ms,那么在读取完第一个字节后,串口开始等待1ms,如果1ms之内接收到了第二个字节,就读取第二个字节,间隔超时重置为0并计时,等待第三个字节的到来。如果第三个字节到来的时间超过了1ms,那么ReadFile()函数立即返回,这时候总超时计时是没到20秒的。

如果在20秒总计时时间结束之前,所有的数据都遵守数据间隔为1ms的约定并陆陆续续的到达串口缓冲区,那么就成功进行了一次串口传输和读取;如果20秒总计时时间到,串口还陆陆续续的有数据到达,即使遵守字节间隔为1ms的约定,ReadFile()函数也会立即返回,这时候总超时就起作用了。

总结,总超时在两种情况下起作用:
第一:串口没进行数据传输,等待总超时时间那么长ReadFile()才返回。非正常数据传输
第二:数据太长,总超时设置太短,数据还没读取完就返回了。读取的数据是不全的。

间隔超时触发是有条件的:
第一:在总超时时间内。
第二:串口进行了数据的传输。
成功的进行一次串口数据的传输和读取,只有总超时和间隔超时相互参与配合才能完成。

发送数据

串口数据发送多作为写文件处理的,程序员可以应用文件操作函数发送数据到串口。采用WriteFile函数发送数据到串口。

BOOL
WINAPI
WriteFile(_In_ HANDLE hFile,_In_reads_bytes_opt_(nNumberOfBytesToWrite) LPCVOID lpBuffer,_In_ DWORD nNumberOfBytesToWrite,_Out_opt_ LPDWORD lpNumberOfBytesWritten,_Inout_opt_ LPOVERLAPPED lpOverlapped);
if (WriteFile(handlePort_, // 文件句柄
outputData, // 数据缓冲区指针
sizeBuffer, // 字节数
&length,
NULL) == 0) // 接收成功发送数据长度的指针
{ AfxMessageBox("writing of serial communication has problem."); return FALSE;
}
inline std::string stringToHex(const std::string& data)
{const std::string hex = "0123456789ABCDEF";std::stringstream ss;for (std::string::size_type i = 0; i < data.size(); ++i)ss << "0x" << hex[(unsigned char)data[i] >> 4] << hex[(unsigned char)data[i] & 0xf] << " ";return ss.str();
}......LOGGING_DEBUG("send:%s", stringToHex(send).data());std::cout << "->send:" << stringToHex(send) << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(5));PurgeComm(m_port, PURGE_RXCLEAR | PURGE_TXCLEAR);DWORD dwWrite = 0;DWORD dwRet = WriteFile(m_port, send.c_str(), send.size(), &dwWrite, NULL);PurgeComm(m_port, PURGE_RXCLEAR | PURGE_TXCLEAR);if (!dwRet){errorCode = base_module_error::make_error_code(base_module_error::BaseModuleErrorCode::serial_port_write_error);return;}

接收数据

串口数据接收多作为读文件处理。程序员可以应用文件操作函数从串口接收数据。用ReadFile函数接收串口的数据。

if (ReadFile(handlePort_, // 句柄
inputData, // 数据缓冲区指针
sizeBuffer, // 字节数
&length, // 指向已经读入的字节数
NULL) == 0) // 重叠I/O结构体
{ AfxMessageBox("Reading of serial communication has problem."); return FALSE;
}
  char _buf;DWORD dwRead;//clock_t begin,end;auto begin = std::chrono::system_clock::now();BOOL bReadOK = ReadFile(m_port, &_buf, 1, &dwRead, NULL);auto spend = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now() - begin).count();if(spend != 0){//计算花费时间LOGGING_DEBUG("spend:%d ms",spend);}if (bReadOK && (dwRead > 0))return static_cast<uint8_t>(_buf);else{if(GetLastError()!=0){LOGGING_ERROR("ReadFile error,GetLastError:%u", GetLastError());std::cout << "ReadFile error,GetLastError:" << GetLastError() << std::endl;}errorCode = base_module_error::make_error_code(base_module_error::BaseModuleErrorCode::serial_port_read_error);return 0;}std::string WindowsSerialPort::getFrameData(const std::string& sd)
{char sendDataArray[50];memset(sendDataArray, 0xFC, sizeof(sendDataArray));char* pSendDataArray = sendDataArray;memcpy((pSendDataArray + 3), sd.c_str(), sd.size());uint8_t crcHi, crcLo;auto* pDat = reinterpret_cast<uint8_t*>(const_cast<char*>(sd.data()));CRCCal(pDat, sd.size(), &crcHi, &crcLo);sendDataArray[sd.size() + 3] = (char)(crcLo & 0x00FFU);sendDataArray[sd.size() + 4] = (char)(crcHi & 0x00FFU);std::string _send(pSendDataArray, sd.size() + 3 + 2);pSendDataArray = nullptr;pDat = nullptr;return _send;
}void WindowsSerialPort::CRCCal(uint8_t* pnt, uint8_t len, uint8_t* CrcHi, uint8_t* CrcLo)
{uint8_t i;uint16_t crc = 0;while (len-- != 0){for (i = 0x80; i != 0; i /= 2){if ((crc & 0x8000U) != 0){crc *= 2;crc ^= 0x1021U;}else{crc *= 2;}if ((*pnt & i) != 0)crc ^= 0x1021U;}pnt++;}*CrcHi = crc >> 8U;*CrcLo = crc & 0xFFU;
}

关闭串口

可以调用CloseHandle API函数关闭串口。

if(CloseHandle(handlePort_) == 0) // 调用该函数关闭串口
{ AfxMessageBox("Port Closeing isn''t successed."); return FALSE;
}

附CMakeLists.txt文件

cmake_minimum_required(VERSION 3.12)project(mywincom VERSION 0.0.1)set(CMAKE_CXX_STANDARD 11)IF(WIN32)SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /SUBSYSTEM:WINDOWS")
ENDIF(WIN32)####################  QT dependencies ####################
#set(CMAKE_CXX_STANDARD 11)
#set(CMAKE_AUTOMOC ON)
#set(CMAKE_AUTORCC ON)
#set(CMAKE_AUTOUIC ON)#set(QT_VERSION 5)
#set(REQUIRED_LIBS Core)
#set(REQUIRED_LIBS_QUALIFIED Qt5::Core)####################  set output directory ####################
set(BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/build)
set(LIB_DIR ${BUILD_DIR}/Release)
set(LIB_FIX)
if (CMAKE_BUILD_TYPE MATCHES "Debug")set(LIB_DIR ${BUILD_DIR}/Debug)set(LIB_FIX _d)
endif ()get_filename_component(ABSOLUTE_PATH ${LIB_DIR} ABSOLUTE)
set(LIB_DIR ${ABSOLUTE_PATH})set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${LIB_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${LIB_DIR}/lib)
set(CMAKE_PDB_OUTPUT_DIRECTORY ${LIB_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${LIB_DIR}/lib)set(LIB_DIR_FIX ${LIB_DIR}/bin)
option(USE_VS_BUILD "use visual studio build." OFF)
if (USE_VS_BUILD)set(LIB_DIR_FIX ${LIB_DIR}/bin/Debug)
endif ()####################  set include path ####################
set(SRC_PATH${CMAKE_CURRENT_SOURCE_DIR}/src${CMAKE_CURRENT_SOURCE_DIR}/src/misc${CMAKE_CURRENT_SOURCE_DIR}/src/protocol)include_directories(${SRC_PATH}${CMAKE_CURRENT_SOURCE_DIR}/src/misc${CMAKE_CURRENT_SOURCE_DIR}/src/protocol${BUILD_DIR}/../include
)add_definitions()####################  scan source files ####################
foreach (path ${SRC_PATH})aux_source_directory(${path} SRC_FILES)
endforeach ()####################  version config ####################
#configure_file(${BUILD_DIR}/../include/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/plugin_version.h)
#include_directories(${CMAKE_CURRENT_BINARY_DIR})#if (MSVC)
#    set(MY_VERSIONINFO_RC "${CMAKE_CURRENT_BINARY_DIR}/VersionInfo.rc")
#    configure_file("${CMAKE_CURRENT_SOURCE_DIR}/resource.rc.in"
#            "${MY_VERSIONINFO_RC}")
#endif ()#add_library(${PROJECT_NAME} SHARED ${SRC_FILES} ${MY_VERSIONINFO_RC})
#add_executable(${PROJECT_NAME} WIN32 ${SRC_FILES})
add_executable(${PROJECT_NAME} ${SRC_FILES})
####################  set target properties ####################
set_target_properties(${PROJECT_NAME} PROPERTIES DEBUG_POSTFIX _d)####################  set target dependencies ####################
#find_package(g3log CONFIG REQUIRED)
#find_package(GTest CONFIG REQUIRED)
#find_package(Qt${QT_VERSION} COMPONENTS ${REQUIRED_LIBS} REQUIRED)
find_library(LibConfig libconfig++)
message(STATUS ${LibConfig})
find_package(unofficial-breakpad CONFIG REQUIRED)set(LOGGING_LIB ${LIB_DIR}/lib/Logging${LIB_FIX}.lib)
set(THIRD_LIBS${LOGGING_LIB}unofficial::breakpad::libbreakpadunofficial::breakpad::libbreakpad_client)
#target_link_options(${PROJECT_NAME} PRIVATE -mwindows)
target_link_libraries(${PROJECT_NAME} PRIVATE ${THIRD_LIBS} ${LibConfig})####################### copy interface file ###############################################
file(COPYsrc/misc/config.hppDESTINATION ${BUILD_DIRECTORY}/include)file(COPYconfig.confDESTINATION ${BUILD_DIRECTORY}/conf)

关于内存泄漏

虚拟内存是您的程序处理的内容。它由所有由malloc,new等人返回的地址组成。每个进程都有自己的虚拟地址空间。虚拟地址的使用在理论上受到程序地址大小的限制:32位程序具有4GB的地址空间; 64位程序有更多。实际上,一个进程可以分配的虚拟内存量少于这些限制。

物理内存是芯片焊接到您的主板,或安装在您的内存插槽。在任何给定时间使用的物理内存量都限制在计算机中的物理内存量。

虚拟内存子系统将您的程序使用的虚拟地址映射到CPU发送到RAM芯片的物理地址。
在任何特定时刻,大部分分配的虚拟地址都是未映射的;因此物理内存使用低于虚拟内存使用。
如果访问已分配但未映射的虚拟地址,操作系统会无形地分配物理内存并将其映射。
如果不访问虚拟地址,操作系统可能会取消映射物理内存。
应用程序一般使用malloc,calloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序未调用对应的free或delete释放该内存块,从而导致内存泄漏。

所以内存泄漏会导致进程的虚拟内存使用不断增加,而由于申请后未被访问使用,所以并不会映射到物理内存占用电脑内存资源(这样系统可以防止某个恶心进程占着茅坑不拉屎…)。
当是如果程序运行的事件够长,每次泄漏不断累积,长期下去虚拟内存空间也会被全部用完,此时系统会使用系统的硬盘存储充当虚拟内存的扩展,如使用C盘空间(操作据编译器不同而不同),所以这样漏下去还是会导致电脑系统卡顿甚至蓝屏。

引用

COMMTIMEOUTS主要用于串口超时参数设置_liuzhuomju的博客-CSDN博客

串口通讯超时的设置与含义(COMMTIMEOUTS) | 超时

ReadFile function (fileapi.h) - Win32 apps | Microsoft Docs

使用Win32创建串口通讯程序-ztemt-ChinaUnix博客

关于内存的两个概念

虚拟内存以及进程的虚拟内存分布(第六章)_ivy_0709的博客-CSDN博客_虚拟内存分布

内存泄漏、虚拟内存、物理内存的联系

串口之ReadFile、WriteFile函数详解_一苇渡江694的博客-CSDN博客_readfile

串口之ReadFile、WriteFile函数详解_一苇渡江694的博客-CSDN博客_readfile

windows下的串口编程,串口操作类封装相关推荐

  1. # 串口编程(SerialPort类)

    串口编程(SerialPort类) 转自https://www.cnblogs.com/xinai/ 近期由于项目中用到串口编程,而以前有从未使用过,查阅相关资料,先将串口编程整个过程整理如下,以备不 ...

  2. python windows 消息通讯_在windows下使用python进行串口通讯的方法

    在windows下使用python进行串口通讯的方法 Windows版本下的python并没有内置串口通讯的pyserial的库,所以需要自己下载.参照了网上的教程,有许多用的pip的安装方式,但是试 ...

  3. Windows下使用Python实现串口通信

    Windows下使用Python实现串口通信 基本信息 配置过程 配置思路 详细配置过程 安装USB装TTL驱动 配置serial库 获取设备端口号 配置串口通信 配置说明 测试过程 其他学习记录 参 ...

  4. Windows下的网络编程Winsock

    文章目录 前言 1.服务器下的Winsock 1.1.构建编程环境: 1.2.WSAData结构体 1.3.WSAStartup初始化Winsock 1.4.WSACleanup释放Winsock 1 ...

  5. 串口编程 - 串口简介

    串口编程 - 串口简介 如需转载请标明出处:http://blog.csdn.net/itas109 QQ技术交流群:129518033 文章目录 串口编程 - 串口简介 前言 1.串口常见的电气标准 ...

  6. (6)Windows下的网络编程

    目录索引 Windows下的网络编程 接口分析 编程差异 编程示例 多路复用 Windows下的网络编程 接口分析 Windows下的socket编程接口与Linux中几乎相同: 不同之处: – 返回 ...

  7. windows 下实现socket编程_传送文件

    windows 下实现socket编程_传送文件 其实,Windows下的socket编程与Linux下的类似,这里不再赘述! 参考:http://blog.csdn.net/chudongfang2 ...

  8. 串口编程(SerialPort类)

    近期由于项目中用到串口编程,而以前有从未使用过,查阅相关资料,先将串口编程整个过程整理如下,以备不时之需. SerialPort类简述 此类位于System.IO.Ports命名空间下.用于控制串行端 ...

  9. 网络——Windows下的网络编程入门

    作者:小 琛 欢迎转载,请标明出处 引言:之前关于网络编程的博文都是基于Linux环境下,而因为后续工作的原因,开发环境为Windows故而学习该内容.实际学习中发现,整个编程思路和流程并没有很大差别 ...

最新文章

  1. 2018年技术展望--中文版
  2. java 硬件交互_Java关键字之native
  3. cmd html 查找汉子字,字符串查找 cmd find命令
  4. runlevel的修改方法
  5. React.js开发生态系统概览 [译-转]
  6. 史上最全面,清晰的SharedPreferences解析
  7. node包管理器npm常用命令
  8. 基于android的课程画图设计,基于Android的智能终端通信勘察设计系统绘图模块的设计与实现...
  9. sql查看数据库线程数_SQL Server始终在可用性组数据库上的最大辅助线程
  10. Required String parameter 'images' is not present
  11. web.config 测试账号
  12. 由ViewStateException: The client disconnected想到的
  13. 阶段5 3.微服务项目【学成在线】_day01 搭建环境 CMS服务端开发_07-CMS需求分析-SSI服务端包含技术...
  14. 代码制作数字流星雨_C语言实现流星雨
  15. Inno Setup 软件安装包制作
  16. html连接有道词典api,调用网易有道词典api
  17. ASP.NET Core3.1 入门介绍
  18. 计算机 随机分组的方法,最小化随机分组方法介绍及其SAS实现
  19. 2020iOS开发工程师面试题汇总(内含面试技巧)-看完BATJ面试官对你竖起大拇指!
  20. AI+社区智能管理,赋能智慧城市人情共「智」

热门文章

  1. 【PC工具】白领办公必备,电脑定时提醒休息护眼软件:眼睛护士
  2. 【FPGA】数码管电子时钟(可设置时间和闹钟)
  3. Dev-C++ 配置 WinPcap 开发环境
  4. html去除图片边框颜色,在PS中怎样做,可以将图片去除所有颜色,只留边框(描边)?...
  5. Guns二次开发(四):重构字典管理模块
  6. java计算机毕业设计养老院管理系统源码+数据库+系统+lw文档+部署
  7. 2019年江苏大学885编程大题
  8. 遇到卸磨杀驴的老板怎么办?和老板两个人从无到有创业,自己累死累活,团队越来越大,结果最近因为业务萎缩要优化我!...
  9. 利用C语言实现sin(x)曲线与cos(x)曲线图形的同时显示
  10. 青岛科技大学计算机科学与技术排名,2017青岛科技大学在全国排名第几