普通视图

近期使用QOSAddSocketToFlow()在Windows下建立QoS踩过的坑

2025年12月25日 15:18

客户要求给原有Socket 通信增加QoS 功能,包括了Server 端和Client 端。示例代码似乎平平无奇,实装却花了三周半。尤其是最后的那个问题,困扰了我半个月。今天终于解决了,简单记录一下,希望能帮到需要的人。

第①个问题

现象:CreateQosHandle() 失败,GetLastError() = ERROR_NOT_SUPPORTED(50)
原因:CreateQosHandle()只有两个参数,出现这个错误是因为有的例子太老了,给第一个参传了{1, 1}。实际上进入Win10 时代之后第一个参就只能传{1, 0}。
解决办法:CreateQosHandle()第一个参传{1, 0}。

第②个问题

现象:QoSAddSocketToFlow() 失败,GetLastError() = WSA_INVALID_PARAMETER(87)
原因:
1)某些例子太老,第5个参传了0。新版本函数只能传两个定义好的宏:QOS_NON_ADAPTIVE_FLOW 和QOS_QUERYFLOW_FRESH,不能传0。本例的使用场景实际只能传QOS_NON_ADAPTIVE_FLOW。
2)我的PC上有两块网卡,连接内网环境的是第二块网卡。因此第2个参不能传NULL,而要通过给一个SOCKADDR结构体赋值IP 地址和Port 的方式,指定使用的网卡。
解决办法:第2个参在握手成功后过CAsyncSocket 的 GetPeerName() 取得连接用的IP地址和端口号,第5个参固定传QOS_NON_ADAPTIVE_FLOW。

第③个问题

现象:同时启动Server 和Client,Client 调用QoSAddSocketToFlow() 失败,GetLastError() = ERROR_NOT_FOUND(1168)
原因:添加QoS 的Socket 不支持用自己的Client 连接自己的Server。
解决办法:再找一台开发器。
P.S: 这个就是我上次吐槽AI的事件。

第④个问题

现象:在确定Socket握手成功的回调函数中添加QoS,Client 调用QoSAddSocketToFlow() 失败,GetLastError() = ERROR_ACCESS_DENIED(5)
原因:添加了QoS 的Server 需要管理员权限运行。
解决办法:调试时Server 端用管理员执行Visual Studio,或者给工程属性–Linker–Manifest File–UAC level 改成【requireAdministrator(/level=’requireAdministrator’)】。

第⑤个问题

现象:Client 的OnConnect(int nErrorCode) 回调中,有时nErrorCode = WSAEWOULDBLOCK(10035)
原因:这不是问题。只是Socket握手过程发生了延时。
解决办法:点两滴眼药水。

第⑥个问题

现象:Socket 连接建立后,Server 端立刻收到OnClose() 回调,并且传入的参数 = WSAECONNABORTED(10053)。
原因:既存的工程在Client创建socket的时候,立刻调用了SetSockOpt()设置了SO_LINGER,并且设定的值是{1, 0},目地是Socket Close 时不等待缓存,直接进行硬关闭。但是这个属性如果在socket握手成功前被设定,那么在调用QoSAddSocketToFlow() 的同时就会产生这样的关闭。
解决办法:将SetSockOpt 的调用时机改到socket 连接建立之后,亦即,Client 端在OnConnect(0)后调用,Server 端在OnAccept(0) 后调用。

其它说明

10035本身不用管,但是跟10053长得太像了。
Server 端也不能用127.0.0.1,不知是否跟多网卡有关。
问题③、④和问题⑥干扰的选项太多,一度非常怀疑杀毒软件、防火墙、域策略,非常混乱。
问题①、②都是通过比较不同的例子找到的破绽。
问题③靠的是CSDN上的一句吐槽。
问题④最终解决靠的是在Git上广搜例子,在一个示例的说明里看到Server侧需要在管理员权限下运行的提示,方解决。
问题⑥最后是用了排除法编程,逐行注代码的笨办法筛出来的。全网没有人遇到同样的问题。可能就没有人提前设SO_LINGER 吧……

示例代码

共通类,继承CAsyncSocket:

#pragma once
#include <afxsock.h>
#include <qossp.h>
#include <winsock2.h>
#include <qos2.h>
#include <iostream>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "qwave.lib")

class CCommonQosSocket : public CAsyncSocket
{
public:
    CCommonQosSocket()
        : m_hQos(NULL)
        , m_dwFlowId(0)
        , m_ver({1, 0}){}
    virtual ~CCommonQosSocket() {
        CloseWithQos();
    }

    BOOL CreateQosHandle() {
        if (m_hQos) {
            QOSCloseHandle(m_hQos);
            m_hQos = NULL;
        }
        if (!QOSCreateHandle(&m_ver, &m_hQos)) {
            int nLastError = GetLastError();
            return FALSE;
        }
        return TRUE;
    }

    BOOL GetPeerAddr(SOCKADDR_IN& peerAddr) {
        int len = sizeof(peerAddr);
        if (!GetPeerName((SOCKADDR*)&peerAddr, &len)) {
            int n = GetLastError();
            return FALSE;
        }
        return TRUE;
    }

    BOOL AddQosFlow(QOS_TRAFFIC_TYPE trafficType, SOCKADDR* pAddr) {
        if (!m_hQos || !m_dwFlowId) {
            return FALSE;
        }

        SOCKADDR* pTgtAddr(pAddr);
        SOCKADDR_IN peerAddr{};
        if (!pTgtAddr) {
            if (!GetPeerAddr(peerAddr)) {
                return FALSE;
            }
            pTgtAddr = static_cast<SOCKADDR*>(&peerAddr);
        }

        BOOL bRet = QOSAddSocketToFlow(m_hQos,
                                  static_cast<SOCKET>(*this),
                                  pTgtAddr,
                                  trafficType,
                                  QOS_NON_ADAPTIVE_FLOW,
                                  &m_dwFlowId);
        int nLastError = GetLastError();
        return bRet;
    }

    void CloseWithQos() {
        if (m_dwFlowId && m_hQos) {
            QOSRemoveSocketFromFlow(m_hQos,
                                    static_cast<SOCKET>(*this),
                                    m_dwFlowId, 
                                    0);
        }

        if (m_hQos) {
            QOSCloseHandle(m_hQos);
            m_hQos = NULL;
        }
        m_dwFlowId = 0;
        __super::Close();
    }

private:
    HANDLE      m_hQos;
    DWORD       m_dwFlowId;
    QOS_VERSION m_ver;
};

Server端部分代码:

#include "CommonQosSocket.h"

class CClientSocket: public CCommonQoSSocket {
};

class CListenSockt : public CCommonQoSSocket {
public:
    virtual void OnAccept(int nErrorCode) override {
        CAsyncSocket::OnAccept(nErrorCode);
        CClientSocket* pNewClient = new CClientSocket;
        sockaddr addr;
        int iAddrLen = sizeof(addr);
        if (Accept(*pNewClient, &addr, &iAddrLen)) {
            pNewClient->CreateQosHandle();
            if (pNewClient->AddQosFlow(QOSTrafficTypeBestEffort, &addr))
                //sccess;
                linger closeLinger{1,0};
                (void)pNewClient->SetSockOpt(SO_LINGER, (const void*)&closeLinger, sizeof linger);
            else {
                //failed
            }
        }
    };
};

void CQoSServerDlg::OnBnClickedButtonStart()
{
    CClientSocket* pListen = new CClientSocket; 
    CString csLocalIP(L"192.168.8.4");
    int nListenPort(32000);

    if (!pListen->Create(nListenPort,
        SOCK_STREAM, FD_READ | FD_WRITE | FD_ACCEPT | FD_CLOSE,
        csLocalIP)) {
        int nError = GetLastError();
        AfxMessageBox(L"Listen Failed.");
        return;
    }

    if (!m_ListenSock.Listen()) {
        AfxMessageBox(L"Listen Failed.");
        return;
    }
}

Client端部分代码:

#include "CommonQosSocket.h"

class CClientSocket: public CCommonQoSSocket {
public:
    virtual void OnConnect(int nErrorCode) overwride {
        CAsyncSocket::OnConnect(nErrorCode);
        if (nErrorCode) {
            return;
        }
        CreateQosHandle();
        if (this->AddQosFlow(QOSTrafficTypeBestEffort, nullptr)) {
            //success
            linger closeLinger{1,0};
            (void)this->SetSockOpt(SO_LINGER, (const void*)&closeLinger, sizeof linger);
        }
        else {
            //failed
        }
    }
};

void CQoSClientDlg::OnBnClickedButtonConnect()
{
    CClientSocket* pClient = new CClientSocket;
    pClient->CreateQosHandle();
    CString csLocal(L"192.168.8.11");
    CString csServer(L"192.168.8.4");
    int nPort(32000);
    pClient->Create(0, SOCK_STREAM, FD_READ | FD_WRITE | FD_CONNECT | FD_CLOSE,
        csLocal);
    if (m_sock.Connect(csServer, nPort)) {
    }
    else {
    }
}

就酱紫,找到问题⑥的原因花了13天,改掉只需要5分钟。


  • (1):就是LSD

如何让MFC的Dialog类型窗口在高度超出屏幕高度时出现比例合适的垂直滚动条

2025年8月20日 22:31

客户提了个需求:因为他们的显示器(32吋)大,所以经常把缩放比设成125%或者150%,希望我们的APP在这两个缩放比下能够正常显示。
但是我们干活用的只是普通的24吋,设成150%之后高度就出溢出屏幕了,这就需要加滚动条。而工作这个东西,到了二鬼子领导那里就会加码,变成100%-225%都得能正常运行,并且因为增加的高度与原来的高度相比没多太多,所以要大滑块,不要分的细碎的小滑块。
这个功能本身不难。通常的做法是取屏幕放大后的窗口新高度,然后减去桌面有效视窗高度,得到的差值除以一个系数,然后用SetScrollRange的第三个数给传进去。然后重写OnVScroll方法,从系数反推滑块位置。
但是,这样得到的是小滑块,而且最后一屏的空白部分也不准确,往往会出现大片空白。

研究了好几天,终于找到了还算不错的方案。在此分享一下。
注意,我只写了垂直滚动条,因为我们的窗体就是瘦长型,即使增加到225%也没超出屏幕宽。给公家干活的一个要务就是不干多余的事,所以要添加水平滚动条的自己酌情修改,我这里就不提供了。

开始。

第一步,在OnInitDialog()中,增加垂直滚动条

如需要增加则对垂直滚动条进行初始化。初始化时,不使用简化版的SetScrollRange(),而改用SetScrollInfo()。利用结构体SCROLLINFO的nPage和nMax配合实现大滑块。这里的逻辑是:nPage与nMax的比值也就是滑块占总高度的比值,比值越接近一,滑块越大。nPage和nMax都是相对值,只要二者单位统一即可。方便起见直接使用真实值。
一个很坑的点是nMax不能用窗口Rect的高,而要取最下边控件的下沿,原因未知。
下面是代码:

BOOL CMFCAppDemoDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

        //取窗口位置
	CRect rcThis;
	GetWindowRect(&rcThis);

        //取最下面控件的位置,如果有动态创建的控件,可以遍历取得。
	CRect rcLastButton;
	GetDlgItem(IDCANCEL)->GetWindowRect(rcLastButton);

        //取放大倍数,96.0是100%时候的DPI
	float fScale = static_cast<float>(GetDpiForWindow(m_hWnd)) / 96.0;
        
        //取桌面工作区大小
	CRect rcScreen;
	::SystemParametersInfo(SPI_GETWORKAREA, 0, &rcScreen, 0);

        //对话框的工作区域理想高度:比最后一个控件多一丢丢。
	int nHeightImage = rcLastButton.bottom + rcLastButton.Height() * fScale;

        //如果想象高度比工作区域高,那么将窗口高度设为与工作区等高。
	if (nHeightImage > rcScreen.Height())
	{
		m_blHasVScrollBar = true; //成员变量,用于标记是否有滚动条
		rcThis.bottom = rcThis.top + rcScreen.Height();
		this->MoveWindow(&rcThis, TRUE); //修改Dialog自身高度
		SCROLLINFO si{};
		si.cbSize = sizeof SCROLLINFO;
		si.fMask = SIF_RANGE | SIF_PAGE | SIF_PAGE;
		si.nPage = rcScreen.Height(); //Windows桌面可利用高度作为Page高
		si.nMax = nHeightImage; //窗口高度最大值。
		SetScrollInfo(SB_VERT, &si, TRUE); //激活滚动条
	}
        //否则没用滚动条
	else
	{
		SetScrollRange(SB_VERT, 0, 0, FALSE);
	}

	return TRUE; 
}

第二步,重写WM_VSCROLL的消息响应函数OnVScroll()

没有难点。只要每个消息处理时,nPage与nMax的比例关系一致即可。

BEGIN_MESSAGE_MAP(CMFCAppDemoDlg, CDialogEx)
	ON_WM_VSCROLL()
END_MESSAGE_MAP()

void CMFCAppDemoDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
	CDialogEx::OnVScroll(nSBCode, nPos, pScrollBar);
        //取之前的滚动条信息
	SCROLLINFO si{};
	GetScrollInfo(SB_VERT, &si, SIF_ALL);

        //滚动条上一次的位置
	int nCurPos = si.nPos;
	const int FACTOR(100);
	switch (nSBCode)
	{
	case SB_LINEUP:          //Scroll one line up
		nCurPos -= (si.nPage / 50); //点击一次箭头,或者按一次↑,移动页面的1/50,注意方向
		break;
	case SB_LINEDOWN:           //Scroll one line down
		nCurPos += (si.nPage / 50); //注意方向
		break;
	case SB_PAGEUP:            //Scroll one page up
		nCurPos -= (si.nPage / 50* 20); //PgUp键的处理。所有的响应要统一单位标准即可。注意方向
		break;
	case SB_PAGEDOWN:        //Scroll one page down        
		nCurPos += (si.nPage / 50* 20); //注意方向
		break;
	case SB_THUMBPOSITION:  //Scroll to the absolute position. The current position is provided in nPos
		nCurPos = nPos; //从缩略图直接确认位置
		break;
	case SB_THUMBTRACK:     //Drag scroll box to specified position. The current position is provided in nPos
		nCurPos = nPos; //从滚动条直接确认位置
		break;
	case SB_ENDSCROLL:
		break;
	default:
		break;
	}
        //确认没有超出最小值和最大值范围。最小值一般是0,最大值是nMax - nPage。
	nCurPos = max(si.nMin, min(nCurPos, si.nMax - static_cast<int>(si.nPage)));
        //当位置移动时,滚动窗口内容
	if (nCurPos != si.nPos)
	{
		int nDelta = si.nPos - nCurPos; //注意方向,原始值减目标值
		si.nPos = nCurPos;
		si.fMask = SIF_POS;
		SetScrollInfo(SB_VERT, &si, TRUE); //设滚动条
		ScrollWindow(0, nDelta); //滚动窗口
		UpdateWindow();
	}
}

第三步,重写WM_MOUSEWHEEL的消息响应函数OnMouseWheel()

同样没有难点,只是鼠标滚动一下会转化成多次向上或向下的消息。

BEGIN_MESSAGE_MAP(CMFCAppDemoDlg, CDialogEx)
	ON_WM_MOUSEWHEEL()
END_MESSAGE_MAP()
BOOL CMFCAppDemoDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{
        //确认滚动条有效
	if (!m_blHasVScrollBar) {
		return CDialogEx::OnMouseWheel(nFlags, zDelta, pt);
	}
	CONST INT WHEEL_SCROLL_LINES(3);
	UINT8 ucDirection(SB_LINEUP);
        //根据zDelta方向确定消息重量
	if (zDelta < 0) {
		ucDirection = SB_LINEDOWN;
	}
        //把鼠标滚动值换算成N个箭头消息并发送。次数是没有方向的。
	UINT unLines = (abs(zDelta) * WHEEL_SCROLL_LINES) / WHEEL_DELTA;
	while (unLines--)
	{
		SendMessage(WM_VSCROLL, MAKEWPARAM(ucDirection, 0), 0);
	}
	return TRUE;
}

总之,最难的其实还是开头。nPage与nMax虽然设什么数都可以,但只有用真实值才是最符合拖动规律的。

❌