转载请标明是引用于 http://blog.csdn.net/chenyujing1234

欢迎大家拍砖

在我的一篇文章<<winCE中实现虚拟串口的方法 >>中,讲到在wince 下开发虚拟串口驱动的方法,现在介绍在windows XP下开发虚拟串口的方法。

可以开发一个虚拟串口,将读写请求传递给USB驱动,这样就可以利用现成的串口调试工具向USB设备读取了。

1、DDK串口开发框架

DDK对串口驱动提供了专门接口。只要编写的驱动满足这些接口,并按照串口标准的命名方法,不管是真实的串口设备,还是虚拟设备,Windows操作系统都会认为

这个设备是一个标准的串口设备。用标准的串口调试工具都可以与这个设备进行通信。

1、1 串口驱动的入口函数

本章的实例程序是在HelloWDM驱动的基础上修改而来,入口函数依然是DriverEntry,在DriverEntry函数中指定各种IRP的派遣函数,以及AddDevice 例程、卸载例程等。

/************************************************************************
* 函数名称:DriverEntry
* 功能描述:初始化驱动程序,定位和申请硬件资源,创建内核对象
* 参数列表:
pDriverObject:从I/O管理器中传进来的驱动对象
pRegistryPath:驱动程序在注册表的中的路径
* 返回 值:返回初始化驱动状态
*************************************************************************/
#pragma INITCODE
extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath)
{
KdPrint(("Enter DriverEntry\n"));
pDriverObject->DriverExtension->AddDevice = HelloWDMAddDevice;
pDriverObject->MajorFunction[IRP_MJ_PNP] = HelloWDMPnp;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = HelloWDMDispatchControlp;
pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloWDMCreate;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = HelloWDMClose;
pDriverObject->MajorFunction[IRP_MJ_READ] = HelloWDMRead;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = HelloWDMWrite;
pDriverObject->DriverUnload = HelloWDMUnload;
KdPrint(("Leave DriverEntry\n"));
return STATUS_SUCCESS;
}

其中在AddDevice例程中,需要创建设备对象,这些都是和以前的HelloWDM驱动程序类似。在创建完设备对象后,需要将设备对象指定一个符号链接,该符号链接必须是

COM开头,并接一下数字,如本例就采用了COM7。因为COM1和COM2在有些计算机中有时会被占用,因此,当该设备对象在指定符号链接时,应该避免采用这些名称。

/************************************************************************
* 函数名称:HelloWDMAddDevice
* 功能描述:添加新设备
* 参数列表:
DriverObject:从I/O管理器中传进来的驱动对象
PhysicalDeviceObject:从I/O管理器中传进来的物理设备对象
* 返回 值:返回添加新设备状态
*************************************************************************/
#pragma PAGEDCODE
NTSTATUS HelloWDMAddDevice(IN PDRIVER_OBJECT DriverObject,
IN PDEVICE_OBJECT PhysicalDeviceObject)
{
PAGED_CODE();
KdPrint(("Enter HelloWDMAddDevice\n"));
NTSTATUS status;
PDEVICE_OBJECT fdo;
UNICODE_STRING devName;
RtlInitUnicodeString(&devName,L"\\Device\\MyWDMDevice");
status = IoCreateDevice(
DriverObject,
sizeof(DEVICE_EXTENSION),
&(UNICODE_STRING)devName,
FILE_DEVICE_UNKNOWN,
0,
FALSE,
&fdo);
if( !NT_SUCCESS(status))
return status;
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
pdx->fdo = fdo;
pdx->NextStackDevice = IoAttachDeviceToDeviceStack(fdo, PhysicalDeviceObject);
UNICODE_STRING symLinkName;
RtlInitUnicodeString(&symLinkName,L"\\DosDevices\\COM7");
pdx->ustrDeviceName = devName;
pdx->ustrSymLinkName = symLinkName;
status = IoCreateSymbolicLink(&(UNICODE_STRING)symLinkName,&(UNICODE_STRING)devName);
if( !NT_SUCCESS(status))
{
IoDeleteSymbolicLink(&pdx->ustrSymLinkName);
status = IoCreateSymbolicLink(&symLinkName,&devName);
if( !NT_SUCCESS(status))
{
return status;
}
}
// 设置为缓冲区设备
fdo->Flags |= DO_BUFFERED_IO | DO_POWER_PAGABLE;
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
KdPrint(("Leave HelloWDMAddDevice\n"));
return STATUS_SUCCESS;
}

在创建完符号链接后,还不能保证应用程序能找出这个虚拟的串口设备,还需要进一步修改注册表。具体位置是HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM,可以在这里加入新项目。本例的项目名是MyWDMDevice,类型为REG_SZ,内容是COM7。

在上述步骤后,即在AddDevice例程中创建COM7的符号链接,并且在注册表进行相应设置,系统会认为有这个串口驱动,用任何一个串口调试软件,都可以枚举到

该串口。

1、2  应用程序与串口驱动的通信

其实对于一个真实的串口驱动,或者这个介绍的虚拟串口驱动,都需要遵循一组接口。这组接口由微软事先定义好了,只要符合这组接口,

windows就会认为这是一个串口设备。这里所指的接口就是应用程序发的IO控制码和读写命令,因此对于串口驱动只要对这些IRP的派遣函数编写适当,就能实现一个串口驱动。

首先用IRPTrace看一下,需要对哪些IRP进行处理,笔者加载本章已经介绍的虚拟串口驱动,并用IRPTrace 拦截其IRP处理信息,在打开串口工具后,会发现IRPTrace立刻跟踪到若干个IO控制码,如下所示:

下面依次解释这些IO控制码,理解这些IO控制码,并处理好这些控制码,是编写串口驱动的核心。关于这些IO控制码在ntddser.h文件中,都有相应的定义,并且还有

相应的数据结构定义。

(1)IOCTL_SERIAL_SET_QUEUE_SIZE

这个控制码是应用程序向驱动请求设置串口驱动内部的缓冲区大小,它是向驱动传递SEARIAL_QUEUE_SIZE 数据结构来进行设置的,对于虚拟串口驱动来说,这是不需要

关心的。用IRPTrace可以看出,串口调试工具会向驱动发送的请求是0x400大小的缓冲区大小。

(2)IOCTL_SERIAL_GET_BAUD_RATE

串口调试工具会接着向驱动发送IOCTL_SERIAL_GET_BAUD_RATE命令,这主要是询问驱动这个设备的波特率。驱动应该回应应用程序SEARIAL_BAUD_RATE数据结构,来通知波特率的数值。

(3)IOCTL_SERIAL_GET_LINE_CONTROL

串口调试工具接着向驱动发送IOTCL_SERIAL_GET_LINE_CONTROL命令,这主要是为了返回串口的行控制信息,行控制信息用SERIAL_LINE_CONTROL数据结构表示。

typedef struct _SERIAL_LINE_CONTROL {
UCHAR StopBits;
UCHAR Parity;
UCHAR WordLength;
} SERIAL_LINE_CONTROL,*PSERIAL_LINE_CONTROL;

其中StopBits是停止位,可以是STOP_BIT_1、STOP_BITS_1_5、STOP_BITS_2等取值。

Parity代表校验位,可以是NO_PARITY、ODD——PARITY、EVEN_PARITY、MARK——PARITY、SPACE_PARITY。WorkLength是数据位,可以是5、6、7、8。

  case IOCTL_SERIAL_GET_LINE_CONTROL:
{
*((PSERIAL_LINE_CONTROL)(Irp->AssociatedIrp.SystemBuffer)) = pdx->Lc;
Irp->IoStatus.Information = sizeof(SERIAL_LINE_CONTROL);
break;
}

(4)IOCTL_SERIAL_GET_CHARS

串口调试工具会接着向驱动发送IOCTL_SERIAL_GET_CHARS命令,这个命令是应用程序向驱动请求特殊字符,用来与控制信号握手,用数据结构SERIAL_CHARS表示。

typedef struct _SERIAL_CHARS {
UCHAR EofChar;
UCHAR ErrorChar;
UCHAR BreakChar;
UCHAR EventChar;
UCHAR XonChar;
UCHAR XoffChar;
} SERIAL_CHARS,*PSERIAL_CHARS;

其中EofChar代表是否是传送结束、ErrorChar代码是否传送中有错误、BreadChar代码是否传送有停止等。

(5)IOCTL_SERIAL_GET_HANDFLOW

串口调试工具会接着向驱动发送IOCTL_SRIAL_GET_HANDFLOW命令,这个命令是负责向驱动程序获得串口驱动的握手信号,握手信号用SERIAL_HANDFLOW数据

结构表示:

typedef struct _SERIAL_HANDFLOW {
ULONG ControlHandShake;
ULONG FlowReplace;
LONG XonLimit;
LONG XoffLimit;
} SERIAL_HANDFLOW,*PSERIAL_HANDFLOW;

(6)IOCTL_SERIAL_SET_WAIT_MASK

串口工具会接着向驱动发送IOCTL_SERIAL_SET_WAIT_MASK命令,这个命令主要是设置串口驱动的某些事件发生时,需要向应用程序通知,这些事件包括以下几种事件:

#define SERIAL_EV_RXCHAR           0x0001  // Any Character received
#define SERIAL_EV_RXFLAG           0x0002  // Received certain character
#define SERIAL_EV_TXEMPTY          0x0004  // Transmitt Queue Empty
#define SERIAL_EV_CTS              0x0008  // CTS changed state
#define SERIAL_EV_DSR              0x0010  // DSR changed state
#define SERIAL_EV_RLSD             0x0020  // RLSD changed state
#define SERIAL_EV_BREAK            0x0040  // BREAK received
#define SERIAL_EV_ERR              0x0080  // Line status error occurred
#define SERIAL_EV_RING             0x0100  // Ring signal detected
#define SERIAL_EV_PERR             0x0200  // Printer error occured
#define SERIAL_EV_RX80FULL         0x0400  // Receive buffer is 80 percent full
#define SERIAL_EV_EVENT1           0x0800  // Provider specific event 1
#define SERIAL_EV_EVENT2           0x1000  // Provider specific event 2
case IOCTL_SERIAL_SET_WAIT_MASK:
{
PIRP            pOldWaitIrp;
PDRIVER_CANCEL  pOldCancelRoutine;
pdx->EventMask = *(PULONG)Irp->AssociatedIrp.SystemBuffer;
KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);
pOldWaitIrp = pdx->pWaitIrp;
if (pOldWaitIrp != NULL)
{
pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);
//对以前没有进行完成例程的等待irp,进行完成
if (pOldCancelRoutine != NULL)
{
pOldWaitIrp->IoStatus.Information = sizeof(ULONG);
*(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = 0;
pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;
pdx->pWaitIrp = NULL;
}
else
{
pOldWaitIrp = NULL;
}
}
KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);
if (pOldWaitIrp != NULL)
{
IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);
}
break;
}

当设置新的阻塞事件时,哪果之前有等待的IRP,那么先进行完成。

(7)IOCTL_SERIAL_WAIT_ON_MASK

这个IO控制码是最重要的一个,当串口调试工具通过前面几个IO控制码初始华好后,就会发送这个请求。

在驱动程序中,应该阻塞在那里,即返回PENDING状态,且通过IoSetCancelRoutine设置取消例程,而不是完成这个IRP。

当IOCTL_SERIAL_SET_WAIT_MASK设置的事件中的一项发生时,阻塞状态改为完成,并通知应用程序是哪种事件发生了。

case IOCTL_SERIAL_WAIT_ON_MASK:
{
PDRIVER_CANCEL  pOldCancelRoutine;
KeAcquireSpinLock(&pdx->IoctlSpinLock, &OldIrql);
//等待irp一定被清除,且eventMask一定不为0
if ((pdx->pWaitIrp != NULL) || (pdx->EventMask == 0))
ntStatus = STATUS_INVALID_PARAMETER;
else if ((pdx->EventMask & pdx->HistoryEvents) != 0)
{
// Some events happened
Irp->IoStatus.Information = sizeof(ULONG);
*(PULONG)Irp->AssociatedIrp.SystemBuffer = pdx->EventMask & pdx->HistoryEvents;
pdx->HistoryEvents = 0;
ntStatus = STATUS_SUCCESS;
}else
{
pdx->pWaitIrp = Irp;
ntStatus = STATUS_PENDING;
IoSetCancelRoutine(Irp, DriverCancelWaitIrp);
if (Irp->Cancel)
{
pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
if (pOldCancelRoutine != NULL)
{
ntStatus = STATUS_CANCELLED;
pdx->pWaitIrp = NULL;
}
else
{
IoMarkIrpPending(Irp);
}
}
else
{
IoMarkIrpPending(Irp);
}
}
KeReleaseSpinLock(&pdx->IoctlSpinLock, OldIrql);
break;
}
VOID DriverCancelWaitIrp(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
KdPrint(("DriverCancelWaitIrp\n"));
PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
KIRQL                   OldIrql;
IoReleaseCancelSpinLock(Irp->CancelIrql);
KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);
pExtension->pWaitIrp = NULL;
KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);
Irp->IoStatus.Status = STATUS_CANCELLED;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

关于删除例程的知识可以参考<<驱动程序的取消IRP >>

1、3  写的实现

串口驱动除了需要完成处理IO控制码外,还需要对读写IRP进行处理。一般情况下,作为应用程序的串口调试工具会开启多个线程,其中主线程负责与串口驱动初始化的IO控制码通信。

另外一个很重要的线程就是发送IOCTL_SERIAL_WAIT_ON_MASK请求,对于没有数据传输的情况下,这个IO控制码请求会PENDING在那里,即阻塞。当有传送的请求时,相应的事件被触发,刚才因为IOCTL_SERAIL_WAIT_ON_MASK的IRP被阻塞的线程得以继续运行,

如果应用程序得知该事件是被写入了一个字符,会去发出一个读请求 ,对于驱动则是读的IRP。如果循环过程,从而实现了一个虚拟摄像头回写的例子。

在对于写IRP的派遣函数中,主要是将写的数据存储在设备扩展中,以便以后读的时候将这些内容返回到应用程序。

另外一个很重要的内容,就是阻塞的IO控制苏醒过来。在本例中调用DriverCheckEvent函数,该函数将阻塞的IRP完成,使应用程序的线程得以继续进行。并且这个线程还知道了SERIAL_EV_RXCHAR和SERIAL_EV_RX80FULL事件的到来,从而发起一个读请求,传送到驱动中就是读IRP。

NTSTATUS HelloWDMWrite(IN PDEVICE_OBJECT fdo,
IN PIRP Irp)
{
KdPrint(("HelloWDMWrite\n"));
NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)fdo->DeviceExtension;
// 获得当前IO堆栈
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
// 获取当前IO堆栈的操作字节数
ULONG DataLen = irpSp->Parameters.Write.Length;
// 从IRP的缓冲区中得到数据
PUCHAR pData = (PUCHAR)Irp->AssociatedIrp.SystemBuffer;
KIRQL OldIrql;
PIRP            pOldReadIrp = NULL;
PDRIVER_CANCEL  pOldCancelRoutine;
// 设置IRP的操作字节数
Irp->IoStatus.Information = 0;
ntStatus = STATUS_SUCCESS;
if (DataLen == 0)
{
ntStatus = STATUS_SUCCESS;
}else if (DataLen>COMBUFLEN)
{
ntStatus = STATUS_INVALID_PARAMETER;
}
else
{
KdPrint(("Write\n"));
// 获取自旋锁
KeAcquireSpinLock(&pdx->WriteSpinLock, &OldIrql);
// 复制内存块
RtlCopyMemory(pdx->Buffer,pData,DataLen);
pdx->uReadWrite = DataLen;
if (pdx->pReadIrp != NULL) // drop it out
{
// 记录IRP
pOldReadIrp = pdx->pReadIrp;
// 设置取消函数
pOldCancelRoutine = IoSetCancelRoutine(pOldReadIrp, NULL);
if (pOldCancelRoutine != NULL)
{
pOldReadIrp->IoStatus.Information = 0;
pOldReadIrp->IoStatus.Status = STATUS_SUCCESS;
pdx->pReadIrp = NULL;
}
else
{
pOldReadIrp = NULL;
}
}
// 检查事件
DriverCheckEvent(pdx, SERIAL_EV_RXCHAR | SERIAL_EV_RX80FULL);
//      DriverCheckEvent(pdx, SERIAL_EV_TXEMPTY);
// 释放自旋锁
KeReleaseSpinLock(&pdx->WriteSpinLock, OldIrql);
if (pOldReadIrp != NULL)
IoCompleteRequest(pOldReadIrp, IO_NO_INCREMENT);
}
Irp->IoStatus.Status = ntStatus;
Irp->IoStatus.Information = DataLen;
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return ntStatus;
}
VOID DriverCheckEvent(IN PDEVICE_EXTENSION pExtension, IN ULONG events)
{
KdPrint(("DriverCheckEvent\n"));
PIRP            pOldWaitIrp = NULL;
PDRIVER_CANCEL  pOldCancelRoutine;
KIRQL           OldIrql;
KeAcquireSpinLock(&pExtension->IoctlSpinLock, &OldIrql);
pExtension->HistoryEvents |= events;
events &= pExtension->EventMask;
//相当于设置触发事件
if ((pExtension->pWaitIrp != NULL) && (events != 0))
{
pOldWaitIrp = pExtension->pWaitIrp;
pOldCancelRoutine = IoSetCancelRoutine(pOldWaitIrp, NULL);
//是否已经被cancel掉?
if (pOldCancelRoutine != NULL)
{
// Nein, also Request beenden
pOldWaitIrp->IoStatus.Information = sizeof(ULONG);
*(PULONG)pOldWaitIrp->AssociatedIrp.SystemBuffer = events;
pOldWaitIrp->IoStatus.Status = STATUS_SUCCESS;
pExtension->pWaitIrp      = NULL;
pExtension->HistoryEvents = 0;
}
else
{
//如果cancel掉,就不用IoCompleteRequest了
pOldWaitIrp = NULL;
}
}
KeReleaseSpinLock(&pExtension->IoctlSpinLock, OldIrql);
if (pOldWaitIrp != NULL)
{
KdPrint(("complete the wait irp\n"));
IoCompleteRequest(pOldWaitIrp, IO_NO_INCREMENT);
}
}

1、4  读的实现

对于虚拟串口的读工作,就变得相对简单。因为写IRP会负责通知让阻塞的线程继续运行,并且通知是何种事件的来临。串口调试软件得知SERAIL_EV_RXCHAR这个事件

,因此发起了读事件。在驱动中,就是进入读IRP的派遣函数。

在该派遣函数中,负责将存储在设备扩展中的数据通过IRP传送到应用程序。同时,还需要做一些同步处理。

NTSTATUS HelloWDMRead(IN PDEVICE_OBJECT fdo,
IN PIRP Irp)
{
KdPrint(("HelloWDMRead\n"));
NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success
PDEVICE_EXTENSION pExtension = (PDEVICE_EXTENSION)fdo->DeviceExtension;
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation( Irp );
ULONG BufLen = irpSp->Parameters.Read.Length;
PCHAR pBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer;
KIRQL OldIrql;
PDRIVER_CANCEL pOldCancelRoutine;
Irp->IoStatus.Information = 0;
DbgPrint("DeviceObject:%08X Read\n",fdo);
if (BufLen == 0)
{
ntStatus = STATUS_SUCCESS;
}
else
{
KeAcquireSpinLock(&pExtension->WriteSpinLock, &OldIrql);
// 内存复制
RtlCopyMemory(pBuf,pExtension->Buffer,BufLen);
Irp->IoStatus.Information = BufLen;
if (BufLen==0 && pExtension->pReadIrp==NULL) // nothing, store
{
// 保存IRP
pExtension->pReadIrp = Irp;
Irp->IoStatus.Status = ntStatus = STATUS_PENDING;
// 设置取消函数
IoSetCancelRoutine(Irp, DriverCancelCurrentReadIrp);
// 重新设置取消函数
if (Irp->Cancel)
{
pOldCancelRoutine = IoSetCancelRoutine(Irp, NULL);
if (pOldCancelRoutine != NULL)
{
// Nein, also IRP hier abbrechen
Irp->IoStatus.Status = ntStatus = STATUS_CANCELLED;
pExtension->pReadIrp = NULL;
}
else
{
// 标记IRP挂起   Ja, Cancel-Routine wird Request beenden
IoMarkIrpPending(Irp);
}
}
else
{
IoMarkIrpPending(Irp);
}
}
KeReleaseSpinLock(&pExtension->WriteSpinLock, OldIrql);
}
Irp->IoStatus.Status = ntStatus;
if (ntStatus != STATUS_PENDING)
IoCompleteRequest( Irp, IO_NO_INCREMENT );
return ntStatus;
}

Window XP驱动开发(二十四)虚拟串口设备驱动相关推荐

  1. i.MX 6ULL 驱动开发 二十九:向 Linux 内核中添加自己编写驱动

    一.概述 Linux 内核编译流程如下: 1.配置 Linux 内核. 2.编译 Linux 内核. 说明:进入 Linux 内核源码,使用 make help 参看相关配置. 二.make menu ...

  2. 嵌入式Linux驱动笔记(二十四)------framebuffer之使用spi-tft屏幕(上)

    你好!这里是风筝的博客, 欢迎和我一起交流. 最近入手了一块spi接口的tft彩屏,想着在我的h3板子上使用framebuffer驱动起来. 我们知道: Linux抽象出FrameBuffer这个设备 ...

  3. Linux 驱动开发 三十四:Linux 内核定时器原理

    参考文档: <Cortex -A7 MPCore Technical Reference Manual> 中 Chapter 9:Generic Timer. <ARM ® Arch ...

  4. Linux 驱动开发 六十四:《pwm-backlight.txt》翻译

    文档路径:linux-imx-4.1.15\Documentation\devicetree\bindings\video\backlight\pwm-backlight.txt. PWM 背光绑定. ...

  5. Linux 驱动开发 二十八:读写锁

    参考博客:Linux 内核同步(三):读-写自旋锁(rwlock)_StephenZhou-CSDN博客_linux rwlock 使用 spinlock 保护临界区时,多个读之间无法并发,只能被 s ...

  6. Linux驱动开发(十八)---网络(网卡)驱动学习

    前文回顾 <Linux驱动开发(一)-环境搭建与hello world> <Linux驱动开发(二)-驱动与设备的分离设计> <Linux驱动开发(三)-设备树> ...

  7. Linux驱动开发(十五)---如何使用内核现有驱动(显示屏)

    前文回顾 <Linux驱动开发(一)-环境搭建与hello world> <Linux驱动开发(二)-驱动与设备的分离设计> <Linux驱动开发(三)-设备树> ...

  8. 基于MTD的NAND驱动开发(二)

    基于MTD的NAND驱动开发(二) 基于MTD的NAND驱动开发(三) http://blog.csdn.net/leibniz_zsu/article/details/4977853 http:// ...

  9. JavaWeb开发与代码的编写(二十四)

    JavaWeb开发与代码的编写(二十四) JNDI数据源的配置 数据源的由来 在Java开发中,使用JDBC操作数据库的四个步骤如下: ①加载数据库驱动程序(Class.forName("数 ...

最新文章

  1. linux下安装libsvm_Linux下libsvm的安装及简单练习
  2. PAT甲级1088 Rational Arithmetic:[C++题解]分数的加减乘除
  3. linux c 定时器
  4. PyTorch tensorboard报错:TensorBoard logging requires TensorBoard version 1.15 or above
  5. .Net Micro Framework 快速入门
  6. struts2.3.4 问题
  7. java 省市区三级联动_AJAX省市区三级联动下拉菜单(java版)
  8. ARKit Plane Detection (平面检测)
  9. 聚类分析入门(理论)
  10. 接口获取行政区划代码_调用百度api利用名称查找该名称的省市县以及行政区划代码...
  11. 中国超级城市的新变局
  12. 第三周学习总结和心得
  13. 2018年6月13日任务
  14. 【Ubuntu】【Linux】命令卸载软件
  15. 【计算机专业毕设之基于python猫咪网爬虫大数据可视化分析系统-哔哩哔哩】 https://b23.tv/jRN6MVh
  16. Oracle数据表创建规则
  17. 测试 必问面经华子~
  18. java线程(16)——死锁讲解,白雪公主与灰姑娘抢口红和镜子的案例
  19. 蓝桥杯 试题 基础练习 Sine之舞 c语言
  20. 【毕业设计】大数据疫情可视化分析系统 - python

热门文章

  1. vue+konva.js图片数据标注多边形矩形---demo2.0。添加了其他功能和完善了代码。
  2. 【面试】 CVTE 视源股份 C++ 软件开发 一面
  3. 对xlslib库与libxls库的简易封装
  4. windows安装jdk+tomcat并配置环境
  5. hp ELITEBOOK异常关机解决
  6. SVN入门第三讲——SVN恢复到历史版本
  7. 第一次使用Xshell在服务器上跑程序
  8. Internet大事记,1981-1985
  9. R语言使用rnorm函数生成正太分布数据、使用curve函数绘制根据指定函数绘制指定范围的曲线图、绘制函数曲线图
  10. Element表格Table文字过长悬浮提示很长