使用winsock重叠I/O模型实现简单网络词典【C++,MFC,数据库】
1. 配置ODBC数据源,能够使用C访问数据库,创建数据库ZIDIAN,添加表格ZIDIAN,两列为ENG和CHI
参考https://blog.csdn.net/warrior_1/article/details/106517949?%3E
2. 创建MFC工程,通过可视化界面拖入各自的组件
服务器端
客户端
3. 服务器端对于数据库的操纵
- 创建全局变量
SQLHENV henv = SQL_NULL_HENV;
SQLHDBC hdbc1 = SQL_NULL_HDBC;
SQLHSTMT hstmt1 = SQL_NULL_HSTMT;
RETCODE retcode;
UCHAR szDSN[SQL_MAX_DSN_LENGTH + 1] = "csql", szUID[MAXNAME] = "sa", szAuthStr[MAXNAME] = "sa";//ODBC数据源名,用户名,密码
- 创建连接数据库,断开连接方法,供其他方法调用
void db() {//开始连接
retcode = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &henv);
retcode = SQLSetEnvAttr(henv, SQL_ATTR_ODBC_VERSION,
(SQLPOINTER)SQL_OV_ODBC3,
SQL_IS_INTEGER);
retcode = SQLAllocHandle(SQL_HANDLE_DBC, henv, &hdbc1);
//1.连接数据源
retcode = SQLConnect(hdbc1, szDSN, 4, szUID, 2, szAuthStr, 2);//其中int类型为前面字符串字段的长度
}
void dbexit() {//关闭连接
SQLDisconnect(hdbc1);
SQLFreeHandle(SQL_HANDLE_DBC, hdbc1);
SQLFreeHandle(SQL_HANDLE_ENV, henv);
}
- 刷新按钮/读取数据
MYLIST.DeleteAllItems(); //清理下方的list control 中条目
for (int i = 0; i < 2; i++) {
MYLIST.DeleteColumn(0); //清理列名
}
// TODO: 在此添加控件通知处理程序代码
UpdateData(TRUE);
DWORD dwStyle = MYLIST.GetExtendedStyle();
dwStyle |= LVS_EX_FULLROWSELECT;
dwStyle |= LVS_EX_GRIDLINES;
MYLIST.SetExtendedStyle(dwStyle); //风格
MYLIST.InsertColumn(0, "英文单词", LVCFMT_LEFT); //列名
MYLIST.SetColumnWidth(0, 250); //列宽度
MYLIST.InsertColumn(1, "中文翻译", LVCFMT_LEFT);
MYLIST.SetColumnWidth(1, 250);
db();
UCHAR sql3[21] = "select * from ZIDIAN";//语句
if ((retcode != SQL_SUCCESS) && (retcode != SQL_SUCCESS_WITH_INFO));
else {
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);//申请语句句柄
SQLExecDirect(hstmt1, sql3, 21); //第三个参数为SQL语句长度
char list[15], list2[15];
SQLBindCol(hstmt1, 1, SQL_C_CHAR, list, 15, 0); //倒数第二参数为读取长度,建议与list长度相同
SQLBindCol(hstmt1, 2, SQL_C_CHAR, list2, 15, 0);
do {
retcode = SQLFetch(hstmt1); //下一行信息
if (retcode == SQL_NO_DATA) {
break;
}
MYLIST.InsertItem(0, ""); //插入第一列;C中不会覆盖,只是显示在最前
MYLIST.SetItemText(0, 0 , list);
MYLIST.SetItemText(0, 1, list2);
} while (1);
dbexit();
}
4)对于list control组件中信息与文本框的映射; 右击list control组件,添加变量【方便调用】为MYLIST
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
// TODO: 在此添加控件通知处理程序代码
int sel = MYLIST.GetNextItem(-1, LVIS_SELECTED); //获取选中行号
if (sel < 0)
return;
else { //选中
CString values;
values = MYLIST.GetItemText(sel, 0);//读取
SetDlgItemText(IDC_EDIT1, values);//放入文本框
values = MYLIST.GetItemText(sel, 1);
SetDlgItemText(IDC_EDIT2, values);
}
*pResult = 0;
5) 删除条目(此处判断单词+翻译–也可以直接使用list control中信息作为判断依据,删除选中信息)
CString english, chinese;
GetDlgItemText(IDC_EDIT1, english);
GetDlgItemText(IDC_EDIT2, chinese);
string sqlstr = "delete from ZIDIAN where ENG='";
sqlstr += english;
sqlstr += "' and CHI='";
sqlstr += chinese;
sqlstr += "'";
UCHAR* sql = (UCHAR*)(char*)sqlstr.c_str(); //注意字符格式的转换
int sqlnum = strlen((char*)sql);
db();
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);//申请语句句柄
SQLExecDirect(hstmt1, sql, sqlnum);//执行SQL语句
dbexit();
6) 增加条目
CString english, chinese;
GetDlgItemText(IDC_EDIT1, english);
GetDlgItemText(IDC_EDIT2, chinese);
string sqlstr = "update ZIDIAN set CHI='";
sqlstr += chinese;
sqlstr += "' where ENG='";
sqlstr += english;
sqlstr += "'";
UCHAR* sql = (UCHAR*)(char*)sqlstr.c_str();
int sqlnum = strlen((char*)sql);
db();
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);//申请语句句柄
SQLExecDirect(hstmt1, sql, sqlnum);
dbexit();
7)修改条目【此处以英文单词为主键以及判断依据,故之能修改翻译,想要修改英文单词可以在数据库中添加一个以此递增的行号,以此为组件和判断 依据即可】
CString english, chinese;
GetDlgItemText(IDC_EDIT1, english);
GetDlgItemText(IDC_EDIT2, chinese);
string sqlstr = "delete from ZIDIAN where ENG='";
sqlstr += english;
sqlstr += "' and CHI='";
sqlstr += chinese;
sqlstr += "'";
UCHAR* sql = (UCHAR*)(char*)sqlstr.c_str();
int sqlnum = strlen((char*)sql);
db();
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);//申请语句句柄
SQLExecDirect(hstmt1, sql, sqlnum);
dbexit();
- 服务器提供的网络服务
1) 提供访问的I/O模型,其中postsend方法如下
// 设置I/O类型,增加套节字上的重叠I/O计数
pBuffer->nOperation = OP_WRITE;
pBuffer->pSocket->nOutstandingOps++;
WSABUF buf;
db();
if ((retcode != SQL_SUCCESS) && (retcode != SQL_SUCCESS_WITH_INFO)) //数据库连接失败
{
buf.buf = (char*)"ERROR";
buf.len = strlen("ERROR");
}
else {
string sqlstr = "select CHI from ZIDIAN where ENG ='";
sqlstr += pBuffer->buff;
sqlstr += "'";
UCHAR* sql = (UCHAR*)(char*)sqlstr.c_str();
int sqlnum = strlen((char*)sql);
retcode = SQLAllocHandle(SQL_HANDLE_STMT, hdbc1, &hstmt1);//申请语句句柄
SQLExecDirect(hstmt1, sql, sqlnum);
char chinese[15];
SQLBindCol(hstmt1, 1, SQL_CHAR, chinese, 15, 0);
retcode = SQLFetch(hstmt1);
if (retcode != SQL_NO_DATA) { //查到数据
buf.buf = chinese;
buf.len = 15;
}
else { //未查到数据
buf.buf = (char*)"NULL";
buf.len = strlen("NULL");
}
}
dbexit();
// 投递此重叠I/O
DWORD dwBytes;
DWORD dwFlags = 0;
if (::WSASend(pBuffer->pSocket->s,
&buf, 1, &dwBytes, dwFlags, &pBuffer->ol, NULL) != NO_ERROR)
{
if (::WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
}
return TRUE;
2)创建服务线程ThreadA
void ThreadA(PVOID pvoid) {
int nPort = 8081; //监听端口号
SOCKET sListen =
::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN si;
si.sin_family = AF_INET;
si.sin_port = ::ntohs(nPort);
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1"); //IP地址设为本地
::bind(sListen, (sockaddr*)&si, sizeof(si));
::listen(sListen, 200);
// 为监听套节字创建一个SOCKET_OBJ对象
PSOCKET_OBJ pListen = GetSocketObj(sListen);
// 加载扩展函数AcceptEx
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes;
WSAIoctl(pListen->s,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&GuidAcceptEx,
sizeof(GuidAcceptEx),
&pListen->lpfnAcceptEx,
sizeof(pListen->lpfnAcceptEx),
&dwBytes,
NULL,
NULL);
// 创建用来重新建立g_events数组的事件对象
g_events[0] = ::WSACreateEvent();
// 在此可以投递多个接受I/O请求
for (int i = 0; i < 5; i++)
{
PostAccept(GetBufferObj(pListen, BUFFER_SIZE));
}
//为重建事件对象数组作准备,即要先构造要在事件对象上等待受信的事件对象数组,但开始时
//事件对象数组为空,要调用RebuildArray()才能设置等待事件,而只有当g_events[0]为受信时
//才会调用RebuildArray(),这里把第一个事件设为“受信”,由于第一个事件对象并没有与任何I/O
//绑定在一起,所以要靠手动设置第一个事件的状态值,在后面的While循环中才会执行:
::WSASetEvent(g_events[0]);
while (TRUE)
{
int nIndex =
::WSAWaitForMultipleEvents(g_nBufferCount + 1, g_events, FALSE, WSA_INFINITE, FALSE);
if (nIndex == WSA_WAIT_FAILED)
{
printf("WSAWaitForMultipleEvents() failed \n");
break;
}
nIndex = nIndex - WSA_WAIT_EVENT_0;
for (int i = 0; i <= nIndex; i++)
{
int nRet = ::WSAWaitForMultipleEvents(1, &g_events[i], TRUE, 0, FALSE);
if (nRet == WSA_WAIT_TIMEOUT)
continue;
else
{
//不管该g_events[i]事件对象是否授信,都可把其状态设为“未传信”,为下次通知做准备
::WSAResetEvent(g_events[i]);
// 重新建立g_events数组
if (i == 0)
{
RebuildArray();
continue;
}
// 由受信的事件对象找到对应的缓冲区对象,进而处理这个I/O
PBUFFER_OBJ pBuffer = FindBufferObj(g_events[i]);
if (pBuffer != NULL)
{
if (!HandleIO(pBuffer))
RebuildArray();
}
}
}
}
}
3)启动线程
_beginthread(ThreadA, 0, NULL);
修改按钮是否可见GetDlgItem(按钮名)->ShowWindow(参数);参数SW_HIDE为隐藏,SW_SHOW显示;关闭线程使用_endthread();或其他方法关闭,
5. 客户端程序
1) 打开连接【先开服务器】;给出全局变量
SOCKET s;
WSADATA wd;//获取socket相关信息
创建与服务器端连接socket【此处没有相关的异常处理,可自行添加连接失败方法】:
WSAStartup(MAKEWORD(2, 2), &wd);
//1.创建TCP Socket,流式套接字
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in addr;//不建议使用sockaddr,建议使用sockaddr_in
addr.sin_port = htons(8081);//网络字节序
addr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//网络字节序
addr.sin_family = AF_INET;//地址族
int len = sizeof(sockaddr_in);
connect(s, (sockaddr*)&addr, len);
修改按钮是否可见同上
2)断开连接
closesocket(s);
//清理winsock环境
WSACleanup();
修改按钮是否可见
3)查询过程
CString english;
GetDlgItemText(IDC_EDIT1, english);//从文本框读
string str(english.GetBuffer());//类型转换
int ret;
char* eng = (char*)str.c_str();
int lensent = sizeof(eng);//发送字节个数
send(s, eng, lensent, 0);//发送给服务器端
char recvbuf[20];
ret = ::recv(s, recvbuf, 20, 0); //接受回应消息
recvbuf[ret] = '\0';
CString result;
result.Format("%s", recvbuf); //字符类型转换
SetDlgItemText(IDC_EDIT2, result); //结果填入文本框
注:
1. 对于char*到CString转换报错或其他问题
打开工程属性设置,在高级中将字符集改成未设置;如果有3类警告可以忽略,在常规中将警告等级改成1级或二级即可;关闭SDL检查
2. 对于文本行按下ENTER键,退出界面的问题
右击文本框–>类向导–>虚函数–>在左侧选中PreTranslateMessage–>添加函数–>编辑函数,添加如下代码
BOOL CMFCAppSQLCLIENTDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_ESCAPE)
{
// 如果消息是键盘按下事件,且是Esc键,执行以下代码(什么都不做,你可以自己添加想要的代码)
return TRUE;
}
if (pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN)
{
// 如果消息是键盘按下事件,且是Entert键,执行以下代码(什么都不做,你可以自己添加想要的代码)
return TRUE;
}
return CDialogEx::PreTranslateMessage(pMsg);
}
ps:此代码中基本没有异常处理,可以自行添加;同时代码较为简单,C++初学者可以看看