IOCP接收缓存导致的内存错乱
在用IOCP控件写了一个ERP服务器后,服务器会发生运行3天后,出现莫名的内存错误,用FastMM检测,是本没有内存错误的地方,而且内存错误出现的地方也不固定。这是一个不可重现的Bug,后续通过打日志把错误范围缩小后发现,每次出现内存错误之前都是由于有链接断开释放,因此就加了日志逐步定位到是TSocketHandle释放引起的,具体原因是:在IOCP中,每个Socket连接需要投递一个接收请求,并给出数据存放内存,原来是销毁TSocketHandle的同时,销毁投递接收请求的缓存,这样有可能对象销毁后,IOCP返回一个异步接收消息,会导致写入到已销毁的接收缓存,造成内存被重写,导致内存错误。
解决办法,是用锁和对象分离相同的机制,把接收缓存和对象分离,在释放对象的时候不释放接收缓存,等待超过30分钟后,重新使用这个锁和接受缓存,这样做即可以解决内存错乱问题,也起到了锁和接收缓存的池化处理。
具体代码处理:
投递请求缓存和对象分开,采用是锁和对象分离相同的机制。
{* 客户端对象和锁 *}
TClientSocket = record
Lock: TCriticalSection;
SocketHandle: TSocketHandle;
IocpRecv: TIocpRecord; //投递请求结构体
IdleDT: TDateTime;
end;
PClientSocket = ^TClientSocket;在释放TSocketHandle的时候,只释放对象,投递请求缓存不释放,和锁一起保留,加入到空闲列表中。procedure TSocketHandles.Delete(const AIndex: Integer);
var
ClientSocket: PClientSocket;
begin
ClientSocket := FList[AIndex];
ClientSocket.Lock.Enter;
try
ClientSocket.SocketHandle.Free;
ClientSocket.SocketHandle := nil;
finally
ClientSocket.Lock.Leave;
end;
FList.Delete(AIndex);
ClientSocket.IdleDT := Now;
FIdleList.Add(ClientSocket);
end;在加入对象的时候,检测空闲列表是否有超过30分钟没使用的,如果有则重复利用。function TSocketHandles.Add(ASocketHandle: TSocketHandle): Integer;
var
ClientSocket, IdleClientSocket: PClientSocket;
i: Integer;
begin
ClientSocket := nil;
for i := FIdleList.Count - 1 downto 0 do
begin
IdleClientSocket := FIdleList.Items[i];
if Abs(MinutesBetween(Now, IdleClientSocket.IdleDT)) > 30 then
begin
ClientSocket := IdleClientSocket;
FIdleList.Delete(i);
Break;
end;
end;
if not Assigned(ClientSocket) then
begin
New(ClientSocket);
ClientSocket.Lock := TCriticalSection.Create;
ClientSocket.IocpRecv.WsaBuf.buf := GetMemory(MAX_IOCPBUFSIZE);
ClientSocket.IocpRecv.WsaBuf.len := MAX_IOCPBUFSIZE;
end;
ClientSocket.SocketHandle := ASocketHandle;
ClientSocket.IdleDT := Now;
ASocketHandle.FLock := ClientSocket.Lock;
ASocketHandle.FIocpRecv := @ClientSocket.IocpRecv;
Result := FList.Add(ClientSocket);
end;
CheckDisconnectedClient方法加锁及判断是否正在执行
原来检测释放断开连接的方法如下:
procedure TIocpServer.CheckDisconnectedClient;
var
i: Integer;
begin
FSocketHandles.Lock;
try
for i := FSocketHandles.Count - 1 downto 0 do
begin
if not FSocketHandles.Items[i].SocketHandle.Connected then
begin
FSocketHandles.Delete(i);
end;
end;
finally
FSocketHandles.UnLock;
end;
end;这个方法存在以下问题:
1、对整个FSocketHandles加锁,FSocketHandles.Delete在释放的时候又加了一次锁,如果Delete加锁等待,则导致整个FSocketHandles被锁住,这时再加连接就会等待,造成IOCP无法接收连接,从而存在问题。
2、如果某个TScoketHandle执行很长时间,它的Connected属性为False,则FSocketHandles.Delete会锁住,造成和1相同的问题。
解决办法:
1、不对整个FSocketHandles加锁,我每次查找一个Connected为False的连接,避免一次加两个锁。
2、TSocketHandle增加一个属性标识是否正在执行中,在检测断开连接的时候如果正在执行中则跳过。
具体代码如下:
procedure TIocpServer.CheckDisconnectedClient;
var
iCount: Integer;
ClientSocket: PClientSocket;
function GetDisconnectSocket: PClientSocket;
var
i: Integer;
begin
Result := nil;
FSocketHandles.Lock;
try
for i := FSocketHandles.Count - 1 downto 0 do
begin
if (not FSocketHandles.Items[i].SocketHandle.Connected)
and (not FSocketHandles.Items[i].SocketHandle.Executing) then
begin
Result := FSocketHandles.Items[i];
Break;
end;
end;
finally
FSocketHandles.UnLock;
end;
end;
begin
ClientSocket := GetDisconnectSocket;
iCount := 0;
while (ClientSocket <> nil) and (iCount < 1024 * 1024) do
begin
ClientSocket.Lock.Enter;
try
if Assigned(ClientSocket.SocketHandle) then
FreeSocketHandle(ClientSocket.SocketHandle);
finally
ClientSocket.Lock.Leave;
end;
ClientSocket := GetDisconnectSocket;
Inc(iCount);
end;
end;主要是使用GetDisconnectSocket来返回一个已经断开的连接。TSocketHandle.Executing的赋值只需要在下面方法中赋值即可,因为他执行的进入口和返回口。
procedure TSocketHandle.ProcessIOComplete(AIocpRecord: PIocpRecord;
const ACount: Cardinal);
begin
FExecuting := True;
try
case AIocpRecord.IocpOperate of
ioNone: Exit;
ioRead: //收到数据
begin
FActiveTime := Now;
ReceiveData(AIocpRecord.WsaBuf.buf, ACount);
if FConnected then
PreRecv; //投递请求
end;
ioWrite: //发送数据完成,需要释放AIocpRecord的指针
begin
FActiveTime := Now;
FSendOverlapped.Release(AIocpRecord);
end;
ioStream:
begin
FActiveTime := Now;
FSendOverlapped.Release(AIocpRecord);
WriteStream; //继续发送流
end;
end;
finally
FExecuting := False;
end;
end;
解决这两个稳定性问题后,IOCP支持的ERP服务器已经能支持7*24小时运行。一般当服务器出现稳定性问题后,日志就开始发挥作用,但是太多无用的日志不利于定位到问题点,太少的日志又无法定位,一般写日志的原则是在调用顺序关键点上加日志、继承关键点上加日志、数据流输入输出关键点上加日志;这样出现问题后,可以快速把问题定位到一段代码上,能帮助缩短解决问题的周期。如果出现一个不可重现BUG,能控制在3天解决,出现一个内存错乱BUG,能控制在半个月解决,哪你的日志辅助调试就是有效的。
下载地址:http://download.csdn.net/detail/sqldebug_fan/4510076
免责声明:此代码只是为了演示IOCP编程,仅用于学习和研究,切勿用于商业用途。水平有限,错误在所难免,欢迎指正和指导。邮箱地址:fansheng_hx@163.com
在物料清单采购中,用到excel上传文件解析功能,不过使用poi来解析,发现如果某个单元格为空,则使用poi的官网示例则会被忽略,导致某些非必填的单元格为空,而解析出来则认为不符合格式。找了半天,也没发现poi正确解析的示例和一些资料,只能自己查查excel的格式,然后再解析了。官网地址示例:http://poi.apache.org/spreadsheet/how-to.html#xssf_sax_api
那么我们就看看excel2007的格式了。
1. excel2007是使用xml格式来存储的,把一个excel文件后缀改为.zip,打开之后就直接可以看到一个excel文件对应的xml格式的文件了。
这里面有几部分
<!--[if !supportLists]-->1. 1 <!--[endif]-->对于docProps目录下 这里core是文件的创建时间和修改时间,标题,主题和作者,app是文档的其他属性,文档类型,版本,是否只读,是否共享,安全属性等文档属性信息。
Core.xml <dc:creator></dc:creator> <cp:lastModifiedBy></cp:lastModifiedBy> <dcterms:created xsi:type="dcterms:W3CDTF">2006-09-13T11:21:51Z</dcterms:created> <dcterms:modified xsi:type="dcterms:W3CDTF">2013-06-05T09:28:23Z</dcterms:modified> App.xml <Application>Microsoft Excel</Application> <DocSecurity>0</DocSecurity> <ScaleCrop>false</ScaleCrop> <Company></Company> <LinksUpToDate>false</LinksUpToDate> <SharedDoc>false</SharedDoc> <HyperlinksChanged>false</HyperlinksChanged> <AppVersion>12.0000</AppVersion> ……
2.在xl目录下是文档的具体内容信息
先看workbook.xml
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <fileVersion appName="xl" lastEdited="4" lowestEdited="4" rupBuild="4507" /> <workbookPr filterPrivacy="1" defaultThemeVersion="124226" /> <bookViews> <workbookView xWindow="0" yWindow="90" windowWidth="19200" windowHeight="11640" /> </bookViews> <sheets> <sheet name="Sheet1" sheetId="1" r:id="rId1" /> <sheet name="Sheet2" sheetId="2" r:id="rId2" /> <sheet name="Sheet3" sheetId="3" r:id="rId3" /> </sheets> <calcPr calcId="125725" /></workbook>
workbook.xml文件包含一对<sheets>标签,其中的每个<sheet>元素都代表Excel 2007文件中的一个,工作表的名称就是其name属性的值,这里有三个sheet。
xl/_rels/workbook.xml.rels定义每个sheetid对应的sheet内容文件sheet1.xml,共享的单元格内容文件sharedstring.xml,样式文件style.xml是当前单元格的样式字体,颜色等样式的xml配置。
Theme存放的是当前的设置导航栏的默认样式。这两个看看大概也就能明白。
关键我们看看下面每个sheet的内容格式,
打开一个sheet1.xml看看
<sheetData> <row r="1" spans="1:7" ht="33.75" customHeight="1"> row标签是表示每一行的数据,r表示第几行,其他几个都是这几行的样式 <c r="A1" s="9" t="s">c标签表示每个单元格的内容,这里A1 第一行的第一列,r表示位置,s表示这个单元格的样式, s=9对应style.xml的的index为9的样式即为这个单元格的样式,t=s表示这个单元格有值,里面的v标签即为值的id,id对应到sharedstring.xm里的id对应的值 <v>2</v> </c> <c r="B1" s="10" /> 没有t属性,表示这个单元格没有值设置 <c r="C1" s="10" /> <c r="D1" s="10" /> <c r="E1" s="10" /> <c r="F1" s="10" /> </row> <row r="2" spans="1:7" ht="27.75" customHeight="1"> 第二行 <c r="A2" s="3" t="s"> 第二行第二列 <v>1</v> </c> <c r="B2" s="4" t="s"> <v>5</v> </c> <c r="C2" s="3" t="s"> <v>0</v>
我们找到对应的第一行第一列的值索引为2对应到sharedStrings.xml里面的index的值,这里si从0开始,第三个即为index为2的值,刚好跟我们的excel的A1值符合<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="71" uniqueCount="13"> <si> <t>物料编号</t> <phoneticPr fontId="1" type="noConversion" /> </si> <si> <t>序号</t> <phoneticPr fontId="1" type="noConversion" /> </si> <si> <t>注意:请不要修改表中蓝色区域文字;带有*号字段是必填项;每张物料清单最多只能导入20条物料信息</t> <phoneticPr fontId="1" type="noConversion" /> </si>
而A1的s=9对应的样式style.xml我们也看看,找到cellXfs里面的第9个,不过这里又引用fontid字体样式,borderid样式,numfmtId格式等
<cellXfs count="11"> ...... <xf numFmtId="0" fontId="0" fillId="3" borderId="1" xfId="0" applyFill="1" applyBorder="1"> <alignment vertical="center" /> </xf> <xf numFmtId="0" fontId="2" fillId="0" borderId="2" xfId="0"
组件是针对同一张表中的字段进行映射,作用是将字段多的一张表分成多个实体类来表示。
如:name与user
user表中有first_name及last_name。在实体类中,自定义一个Name类来表示first_name与last_name;
Name实体类:
package cn.framelife.mvc.entity;
public class Name {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
User实体类:
package cn.framelife.mvc.entity;
import java.io.Serializable;
public class User implements Serializable {
private Integer id;
private Name name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
}
User.hbm.xml:
<hibernate-mapping>
<class name="cn.framelife.hibernate.entity.User" table="user" catalog="hibernate">
<id name="id" type="java.lang.Integer">
<column name="id" />
<generator class="native" />
</id>
<component name="name" class="cn.framelife.hibernate.entity.Name">
<property name="firstName" column="first_name"></property>
<property name="lastName" column="last_name"></property>
</component>
</class>
</hibernate-mapping>
增加操作:
tx = session.beginTransaction();
Name name = new Name();
name.setFirstName("111");
name.setLastName("2222");
User user = new User();
user.setName(name);
session.save(user);
tx.commit();