Mono环境下System.Data.Sqlclient无法连接带有实例和端口的SqlServer连接字符串的BUG修复
1、问题现象
本人为了学习租用了一台国外(日本linode)机房的服务器(vps),不要问我为什么,就是喜欢折腾。
服务器作为WebServer,系统为CentOS(linux),为了跑.net网站,使用了Mono的.net环境,和jexus网站容器。系统为CentOS 7 + Mono 3.8.0 + jexus 5.6
AWS有个1年免费的ECS,本着无事找事,并且有便宜白占不白占的原则,也去弄了一年AWS,万般折腾下,配了windows 2008 + Sql Server Express 2005 Adv用做测试数据库服务器,数据库实例名为SQLEXPRESS2005
公司在武汉电信机房有托管服务器,系统为Windows 2008 + IIS + SqlServer 2008,数据库实例为默认实例MSSQLSERVER
上述简称 日本WebServer,新加坡SqlServer,武汉WebServer,武汉SqlServer
前几日,新做了个MVC4测试项目,FTP传到 日本WebServer,数据库附加到 新加坡SqlServer,登陆提交时出现异常信息“sqlserver server does not exist or connection refused.”,然后各种记录log,各种重传DLL重启jexus重启系统,故障信旧,查看微软的在线帮助也没有解答。
然后,把网站上传到公司武汉WebServer,登陆,居然正常,进入后各种与数据库相关的操作,均正常。
这可奇了怪了,怒把数据库附加到武汉SqlServer,修改web.config中的连接字符串,武汉WebServer访问依旧正常,日本Webserver居然也正常!
新加坡SqlServer连接字符串部分connectionString="Data Source=52.74.251.34\SqlExpress2005,14433;
武汉SqlServer连接字符串部分connectionString="Data Source=61.22.65.22,14433;
如下图:

当时的第一思路,新加坡SqlServer或日本WebServer是不是无法互通,或者是否各自把对方的IP加入了黑名单,但经过各种排查,发现网络和防火墙均正常。
然后怒用wireshark对武汉和新加坡的sqlServer进行抓包,发现新加坡SqlServer的数据库端口居然没有入站报文。
网络正常,但又没有数据库入站连接,这真是无计可施。
最后在日本WebServer使用tcpdump进行抓包,down下来一看,突然让人眼前一亮。如下图,服务器居然把Data source的主机部分,识别成了域名,调用了DNS进行解析,但这地址当然是不存在的,这样也就解释了为啥无法连接的问题。

问题果然如此么,此时,对日本WebServer的Web.config中的数据库连接进行测试,原武汉SqlServer的data source部分由"61.22.65.22,11433"修改为"61.22.65.22\MSSQLSERVER,11433",果然也不能成功访问。
2、问题解决
由于SqlClient程序集,被封装在System.Data.dll中,win和Mono的此dll文件内包含的内容不尽相同,所以用win版的System.Data.dll进行替换也无济于事。
从http://download.mono-project.com/sources/mono/mono-3.8.0.tar.bz2下载mono3.8.0源码,找到目录mcs/class/System.Data/System.Data.SqlClient/,打开其中SqlConnection.cs文件,经过一番定位,终于找到了其解析数据库连接的方法ParseDataSource(),代码如下
1 private bool ParseDataSource (string theDataSource, out int thePort, out string theServerName) 2 { 3 theServerName = string.Empty; 4 string theInstanceName = string.Empty; 5 6 if (theDataSource == null) 7 throw new ArgumentException("Format of initialization string does not conform to specifications"); 8 9 thePort = DEFAULT_PORT; // default TCP port for SQL Server 10 bool success = true; 11 12 int idx = 0; 13 if ((idx = theDataSource.IndexOf (',')) > -1) { 14 theServerName = theDataSource.Substring (0, idx); 15 string p = theDataSource.Substring (idx + 1); 16 thePort = Int32.Parse (p); 17 } else if ((idx = theDataSource.IndexOf ('\\')) > -1) { 18 theServerName = theDataSource.Substring (0, idx); 19 theInstanceName = theDataSource.Substring (idx + 1); 20 21 // do port discovery via UDP port 1434 22 port = DiscoverTcpPortViaSqlMonitor (theServerName, theInstanceName); 23 if (port == -1) 24 success = false; 25 } else 26 theServerName = theDataSource; 27 28 if (theServerName.Length == 0 || theServerName == "(local)" || theServerName == ".") 29 theServerName = "localhost"; 30 31 if ((idx = theServerName.IndexOf ("tcp:")) > -1) 32 theServerName = theServerName.Substring (idx + 4); 33 34 return success; 35 }
代码中绿色背景部分,是否有问题呢,当然是有问题的,哈哈~~~
比如,上面的连接字符串,用这个方法解析出来,theServerName=52.74.251.34\SqlExpress2005,当然不会正常运行。
知道了问题,就知道解决方法啦
修改代码如下
private bool ParseDataSource(string theDataSource, out int thePort, out string theServerName)
{
theServerName = string.Empty;
string theInstanceName = string.Empty;
if (theDataSource == null)
throw new ArgumentException("Format of initialization string does not conform to specifications");
thePort = DEFAULT_PORT; // default TCP port for SQL Server
bool success = true;
theServerName = theDataSource;
int idx = 0;
if ((idx = theServerName.IndexOf(',')) > -1)
{
string p = theServerName.Substring(idx + 1);
thePort = Int32.Parse(p);
theServerName = theServerName.Substring(0, idx);
}
if ((idx = theServerName.IndexOf('\\')) > -1)
{
theInstanceName = theDataSource.Substring(idx + 1);
theServerName = theServerName.Substring(0, idx);
if (thePort <= 0)
{
// do port discovery via UDP port 1434
port = DiscoverTcpPortViaSqlMonitor(theServerName, theInstanceName);
if (port == -1)
success = false;
}
}
if (theServerName.Length == 0 || theServerName == "(local)" || theServerName == ".")
theServerName = "localhost";
if ((idx = theServerName.IndexOf("tcp:")) > -1)
theServerName = theServerName.Substring(idx + 4);
return success;
}
到此,故障排查就到此了,这样就结束了么,当然没有
由于mono是在linux编译安装,所以这个改动,还得ssh进服务器进行修改并重新编译才能生效。
使用putty,登陆日本WebServer,找到mono编译的源码目录(辛亏当时编译好后没有删除),依次进入mcs->class->System.Data->System.Data.SqlClient,vi SqlConnection.cs,修改掉上述代码,保存退出。
随便到mono主目录,分别执行make和make install,漫长的等待后,mono即编译成功。
重启jexus服务,/usr/jexus/jws restart,重启成功后,再次访问,新加坡SqlServer成功访问。
据此,这个BUG被修复。
3、感悟
在使用了成熟团队开发的框架时,项目相关部分出现异常,第一时间一定要相信他人的代码,因为很多时候,项目跑不起来,排查到最后可能都是自己犯了某低级错误。
在确定排除掉自己的BUG后,就需要针对异常现象进行全方面的排查了,在这里如果有发现某框架的一些蛛丝马迹,就需要对框架代码进排查了,下源码,反编译,用各种手段看到框架的逻辑代码,有没有问题自然就一清二楚了。

浙公网安备 33010602011771号