概要
C#でデスクトップアプリから外部サーバーへアクセスするためのサンプルコードを記載しました。NET USEを使用して接続する方法と、WindowsAPIを使用して接続する方法を記載しています。
コード
業務で行った対応を元にサンプルコードを記載しています。
業務ではNET USEコマンドでは現在ログイン中のユーザーIDを用いて接続、WindowsAPIではあらかじめ決められたユーザーIDとパスワードを用いて接続、接続できなければユーザーIDとパスワードを入力する画面を表示、という形で実装しましたので、サンプルコードもその形で記載しています。
1. NET USEコマンドを使用して外部サーバーに接続する
NET USEコマンドとは
Windowsコマンドの一つで、ネットワーク上の外部サーバーの共有フォルダに接続したり、切断することができるコマンドです。netコマンドというネットワーク関連を管理できるコマンドの一つです。
public class ServerConnectorByNetUse
{
/// <summary>UNCパス</summary>
private string UncPath { get; set; }
/// <summary>ユーザーID</summary>
private string UserId { get; set; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="uncPath">UNCパス</param>
/// <param name="userId">ユーザーID</param>
public ServerConnectorByNetUse(string uncPath, string userId)
{
UncPath = uncPath;
UserId = userId;
}
/// <summary>
/// サーバーに接続する
/// </summary>
/// <returns>結果 (true : 成功、false : 失敗)</returns>
public bool TryConnect()
{
try
{
using (var open = new Process())
{
// 既に接続済みの場合がある為、一旦接続を解除する
Disconnect();
open.StartInfo.FileName = "cmd.exe"; // コマンド名
open.StartInfo.Arguments = "/c"; // 引数①
open.StartInfo.Arguments += $@"net use {UncPath} /user:{UserId}"; // 引数② (接続コマンド)
open.StartInfo.CreateNoWindow = true; // DOSプロンプトの黒い画面を非表示
open.StartInfo.UseShellExecute = false; // プロセスを新しいウィンドウで起動するか否か
open.StartInfo.RedirectStandardOutput = true; // 標準出力をリダイレクトで取得する
open.StartInfo.RedirectStandardError = true; // エラー出力をリダイレクトで取得する
open.Start();
if (!open.WaitForExit(1000))
{
// タイムアウトした場合
return false;
}
string standerdOutput = open.StandardOutput.ReadToEnd(); // 標準出力のメッセージを取得 (ここではこの変数は使用していません)
string standardError = open.StandardError.ReadToEnd(); // エラー出力のメッセージを取得
int exitCode = open.ExitCode;
if (!string.IsNullOrEmpty(standardError))
{
// エラー出力がある場合
string strErrorCode = GetErrorCode(standardError);
if (int.TryParse(strErrorCode, out int errorCode))
{
// エラーコードをログ出力 (省略)
// 他ユーザーアカウントで既に接続が確立されている場合は接続OKとする
if (errorCode == 1219) return true;
}
}
return exitCode == 0;
}
}
catch (Exception)
{
// エラーハンドル処理 (省略)
}
}
/// <summary>
/// サーバーへの接続を解除する
/// </summary>
public void Disconnect()
{
try
{
using (var open = new Process())
{
open.StartInfo.FileName = "cmd.exe"; // コマンド名
open.StartInfo.CreateNoWindow = true; // DOSプロンプトの黒い画面を非表示
open.StartInfo.UseShellExecute = false; // プロセスを新しいウィンドウで起動するか否か
open.StartInfo.RedirectStandardOutput = true; // 標準出力をリダイレクトで取得したい
open.StartInfo.RedirectStandardError = true; // エラー出力をリダイレクトで取得したい
open.StartInfo.Arguments = "/c"; // 引数①
open.StartInfo.Arguments += $@"net use {UncPath} /delete /no"; // 引数② (切断コマンド)
open.Start();
if (!open.WaitForExit(1000))
{
// タイムアウトした場合
return;
}
string standerdOutput = open.StandardOutput.ReadToEnd(); // 標準出力のメッセージを取得 (ここではこの変数は使用していません)
string standardError = open.StandardError.ReadToEnd(); // エラー出力のメッセージを取得
int exitCode = open.ExitCode;
if (!string.IsNullOrEmpty(standardError))
{
// エラー出力がある場合
string strErrorCode = GetErrorCode(standardError);
if (int.TryParse(strErrorCode, out int errorCode))
{
// エラーコードをログ出力 (省略)
}
}
}
}
catch (Exception)
{
// エラーハンドル処理 (省略)
}
}
/// <summary>
/// エラーメッセージからエラーコードを返却する
/// </summary>
/// <param name="errorMessage">エラーメッセージ</param>
/// <returns>エラーコード</returns>
private string GetErrorCode(string errorMessage)
{
var errorMessageList = errorMessage.Split(' ', '\n').ToList();
foreach (string targetMessage in errorMessageList)
{
if (int.TryParse(targetMessage, out _)) return targetMessage;
}
return string.Empty;
}
}
補足
-
正常に接続が成功すると、
int exitCode = open.ExitCode;
で0
が返却されます。 -
以下のように
open.StartInfo.RedirectStandardOutput = true; open.StartInfo.RedirectStandardError = true;
を行っていないと
string standerdOutput = open.StandardOutput.ReadToEnd(); string standardError = open.StandardError.ReadToEnd();
の部分でエラーとなります。
NET USE接続時のエラーコードについて
エラー出力 (コード上のopen.StandardError.ReadToEnd();
で得られる文字列) がある場合、エラー出力の文字列からエラーコードを抽出しています。(GetErrorCode()
メソッドの部分)
エラーコードは沢山ありますが、よくある?のは下記かと思います。
エラーコード | 説明 |
---|---|
-99 | サーバー接続時にエラーが発生しました。 |
-1 | サーバー接続時にタイムアウトしました。 |
53 | ネットワークパスが見つかりません。 |
67 | ネットワーク名が見つかりません。 |
87 | パラメーターが間違っています。 |
1219 | 同じユーザーによる、サーバーまたは共有リソースへの複数のユーザー名での複数の接続は許可されません。サーバーまたは共有リソースへの以前の接続をすべて切断してから、再試行してください。 |
1223 | この操作はユーザーによって取り消されました。 |
1312 | 指定されたログオン セッションは存在しません。そのセッションは既に終了している可能性があります。 |
1326 | ユーザー名またはパスワードが正しくありません。 |
2250 | このネットワーク接続はありません。 |
参考:システム エラー コード
2. WindowsAPIを使用して外部サーバーに接続する
WNetAddConnection2について
WindowsAPIに「WNetAddConnection2」というメソッドが用意されており、このメソッドを使用することでネットワーク接続が可能です。
public class ServerConnectorByAPI
{
private NETRESOURCE netResource;
[DllImport("mpr.dll", EntryPoint = "WNetAddConnection2", CharSet = CharSet.Unicode)]
private static extern int WNetAddConnection2(ref NETRESOURCE lpNetResource, string lpPassword, string lpUsername, Int32 dwFlags);
[DllImport("mpr.dll", EntryPoint = "WNetCancelConnection2", CharSet = CharSet.Unicode)]
private static extern int WNetCancelConnection2(string lpName, Int32 dwFlags, bool fForce);
/// <summary>UNCパス</summary>
private string UncPath { get; set; }
/// <summary>ユーザーID</summary>
private string UserID { get; set; }
/// <summary>パスワード</summary>
private string Password { get; set; }
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="uncPath">UNCパス</param>
/// <param name="userId">ユーザーID</param>
/// <param name="password">パスワード</param>
public ServerConnectorByAPI(string uncPath, string userId, string password)
{
UncPath = uncPath;
UserID = userId;
Password = password;
}
/// <summary>
/// ネットワーク情報
/// </summary>
[StructLayout(LayoutKind.Sequential)]
public struct NETRESOURCE
{
public int dwScope;
public int dwType;
public int dwDisplayType;
public int dwUsage;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpLocalName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpRemoteName;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpComment;
[MarshalAs(UnmanagedType.LPWStr)]
public string lpProvider;
}
/// <summary>
/// サーバーに接続する
/// </summary>
/// <returns>結果 (true : 成功、false : 失敗)</returns>
public bool TryConnect()
{
try
{
netResource.dwScope = 0; // 列挙の範囲
netResource.dwType = 1; // リソースタイプ
netResource.dwDisplayType = 0; // 表示オブジェクト
netResource.dwUsage = 0; // リソースの使用方法
netResource.lpLocalName = null; // ローカルデバイス名
netResource.lpRemoteName = UncPath; // UNCパス
netResource.lpProvider = null; // プロバイダ名
// 既に接続済みの場合がある為、一旦接続を解除する
WNetCancelConnection2(UncPath, 0, true);
// 接続
int ret = WNetAddConnection2(ref netResource, Password, UserID, 8);
// 他ユーザーアカウントで既に接続が確立されている場合 (1219) は接続OKとする
return ret == 0 || ret == 1219;
}
catch (Exception)
{
// エラーハンドル処理 (省略)
}
}
/// <summary>
/// サーバーへの接続を解除する
/// </summary>
/// <returns>結果 (true : 成功、false : 失敗)</returns>
public void TryDisConnect()
{
try
{
WNetCancelConnection2(UncPath, 0, true);
}
catch (Exception)
{
// エラーハンドル処理 (省略)
}
}
}
補足
- このメソッドでは引数で指定したログインIDとパスワードを使用していますが、接続できなかった場合は資格情報を入力する画面が表示され、ログインIDやパスワードを手入力できます。
- もし資格情報を入力する画面でキャンセルした場合は、
WNetCancelConnection2()
の戻り値としてエラーコード"1223"が返却されます。
使用方法
- NET USEを使用する方法
// 現在ログインしているユーザーIDを使用しNET USEで接続
var connectorByNeteUse = new ServerConnectorByNetUse(serverPath, Environment.UserName);
bool ret = connectorByNeteUse.TryConnect();
- WindowsAPIを使用する方法
// 既定のID/PWを使用しWindowsAPIで接続
// 既定のID/PWでログインできなければ入力画面を表示
var ConnectorByAPI = new ServerConnectorByAPI(toServerPath, userID, password);
bool ret = ConnectorByAPI.TryConnect();
終わりに
既に接続済みの場合は、NET USEやWindowsAPIともに、戻り値が0
ではなく、1219
で返却されます。戻り値が0
以外でも挙動上は問題無い場合があるので、戻り値の知識も最低限は必要になってくるかなと感じました。サーバーへアクセスした後は、サーバー内のファイルの情報を読み取ったり、サーバー内のファイルをコピーしてきたりができると思います。