1. 概要
通常对于一个正在执行的进程而言,我们会关注进程的内存/CPU占用,网络连接,启动参数,映像路径,线程,堆栈等信息。
而通过类似任务管理器,命令行等方式可以轻松获取到这些信息。但是,这些信息究竟是从何而来呢?
- linux
由于linux平台未直接提供进程信息的访问接口,而是通过/proc文件系统向用户展示相关业务信息。虽然也可以通过neklink通信直接从内核获取我们想要的信息,编码复杂性相对较高,因此我们选择使用/proc文件系统的接口。 - Window
windows平台原生提供了大量操作系统相关的接口,但是每个接口的功能相对独立,因此需要找到对应的接口查询数据,最后将数据组装起来。
2. 进程列表
1)linux
/proc/[PID]/
:这是每个正在运行的进程都有一个对应的目录,其中[PID]
是进程的ID号。
DIR *proc = opendir("/proc");
if (proc) {struct dirent *entry;while ((entry = readdir(proc)) != NULL) {if (entry->d_type == DT_DIR) {std::string dir_name(entry->d_name);if (dir_name.find_first_not_of("0123456789") == std::string::npos) {int32_t pid = atoi(entry->d_name);}}}closedir(proc);
}
2)Window
windows平台获取进程列表的方式比较多,只需从中选择任意一种即可。
- CreateToolhelp32Snapshot与Process32First等结合使用;
- EnumProcesses枚举所有进程ID;
- NtQuerySystemInformation查询SystemProcessInformation信息
- ……
在这里,采用第一种方式遍历进程。
当然,由于CreateToolhelp32Snapshot底层实际上同样是通过NtQuerySystemInformation实现的。因此,若追求高效率的话,可以使用第三种方案。
HANDLE hSnapshot =CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS | TH32CS_SNAPTHREAD, 0);
if (hSnapshot) {PROCESSENTRY32 pe32;pe32.dwSize = sizeof(pe32);if (Process32First(hSnapshot, &pe32)) {do {std::string path;W2A(pe32.szExeFile, &path);Proc detail_info;detail_info.pid = pe32.th32ProcessID;detail_info.name = path;// GetDetailInfoproc_map->emplace(pe32.th32ProcessID, detail_info);} while (Process32Next(hSnapshot, &pe32));}CloseHandle(hSnapshot);
}
另附第三种方案实现:
typedef NTSTATUS(NTAPI *P_NT_QUERY_SYSTEM_INFORMATION)(SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation,ULONG SystemInformationLength, PULONG ReturnLength);HMODULE hNtdll = GetModuleHandle(L"ntdll.dll");
P_NT_QUERY_SYSTEM_INFORMATION pfnNtQueryInformationProcess = NULL;
if (hNtdll) {pfnNtQueryInformationProcess =reinterpret_cast<P_NT_QUERY_SYSTEM_INFORMATION>(GetProcAddress(hNtdll, "NtQuerySystemInformation"));ULONG length = 0;PUCHAR pInfo = NULL;do {DWORD result = pfnNtQueryInformationProcess(SystemProcessInformation, pInfo,length, &length);
#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004if (result != 0) {if (result == STATUS_INFO_LENGTH_MISMATCH) {pInfo = new UCHAR[length];continue;}break;}PSYSTEM_PROCESS_INFORMATION _ProcessInfo;ULONG Offset = 0;do {_ProcessInfo = (PSYSTEM_PROCESS_INFORMATION)&pInfo[Offset];int64_t pid = HandleToLong(_ProcessInfo->UniqueProcessId);// ToDoOffset += _ProcessInfo->NextEntryOffset;} while (_ProcessInfo->NextEntryOffset);break;} while (true);if (pInfo) {delete pInfo;}
}
SYSTEM_THREAD_INFORMATION具体结构请参考:SYSTEM_PROCESS_INFORMATION
3. 参数列表
1)linux
/proc/[PID]/cmdline
:这个文件包含了启动该进程的命令行参数。参数之间使用null字符(‘\0’)分隔。
char cmd_path[PROCPATHLEN];
sprintf(cmd_path, "/proc/%d/cmdline", pid);FILE *fp = fopen(cmd_path, "r");
if (fp) {char line[4096];if (fgets(line, sizeof(line), fp)) {imagepath->assign(line);startparamater->assign(line);int32_t offset = startparamater->size() + 1;while (line[offset]) {startparamater->append(" ");startparamater->append(line + offset);offset += strlen(line + offset) + 1;}}fclose(fp);
}
2)Window
windows平台没有直接提供获取进程启动参数的接口,但是可以通过解析进程的PEB(进程环境块)地址,获取信息。
typedef NTSTATUS(NTAPI *NT_QUERY_INFORMATION_PROCESS)(HANDLE, PROCESSINFOCLASS,PVOID, ULONG, PULONG);HMODULE hNtdll = GetModuleHandle(L"Ntdll");
if (hNtdll) {NT_QUERY_INFORMATION_PROCESS pNtQueryInformationProcess =(NT_QUERY_INFORMATION_PROCESS)GetProcAddress(hNtdll,"NtQueryInformationProcess");if (pNtQueryInformationProcess) {// 只读打开进程句柄HANDLE hProcess =OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);if (hProcess) {PWSTR buffer = NULL;do {PROCESS_BASIC_INFORMATION pbi = {0};// 读取进程基本信息RTL_USER_PROCESS_PARAMETERSif (pNtQueryInformationProcess(hProcess, ProcessBasicInformation, (PVOID)&pbi,sizeof(PROCESS_BASIC_INFORMATION), NULL)) {break;}if (NULL == pbi.PebBaseAddress) {break;}PEB peb;SIZE_T dwDummy;// 从PEB地址中读取PEB结构if (!ReadProcessMemory(hProcess, pbi.PebBaseAddress, &peb, sizeof(peb),&dwDummy)) {break;}RTL_USER_PROCESS_PARAMETERS para;// 从参数结构地址读取RTL_USER_PROCESS_PARAMETERS结构if (!ReadProcessMemory(hProcess, peb.ProcessParameters, ¶,sizeof(para), &dwDummy)) {break;}// 从映像文件地址读取映像文件路径LPVOID lpAddress = para.ImagePathName.Buffer;DWORD dwSize = para.ImagePathName.Length;buffer = new WCHAR[dwSize / sizeof(WCHAR) + 1];buffer[dwSize / sizeof(WCHAR)] = 0x00;if (!ReadProcessMemory(hProcess, lpAddress, buffer, dwSize, &dwDummy)) {break;}W2A(buffer, imagepath);delete[] buffer;buffer = NULL;// 从参数列表地址读取参数列表lpAddress = para.CommandLine.Buffer;dwSize = para.CommandLine.Length;buffer = new WCHAR[dwSize / sizeof(WCHAR) + 1];buffer[dwSize / sizeof(WCHAR)] = 0x00;if (!ReadProcessMemory(hProcess, lpAddress, buffer, dwSize, &dwDummy))break;W2A(buffer, startparamater);delete[] buffer;buffer = NULL;result = true;} while (false);if (buffer) {delete[] buffer;}CloseHandle(hProcess);}}
}
4. 动态库
1)linux
/proc/[PID]/maps
:这个文件包含了进程的内存映射信息,显示了进程所使用的内存地址范围及其对应的权限。
char map_path[PROCPATHLEN];
sprintf(map_path, "/proc/%d/maps", pid);
FILE *fp = fopen(map_path, "r");
if (fp) {char line[1024];char filename[1024];std::unordered_set<std::string> module_sets;while (fgets(line, sizeof(line), fp)) {sscanf(line, "%*s %*s %*s %*s %*ld %s", filename);if (filename[0] == '/') {module_sets.emplace(filename);}}fclose(fp);for (std::unordered_set<std::string>::const_iterator itr =module_sets.begin();itr != module_sets.end(); itr++) {ptable->push_back(*itr);}
}
2)Window
windows平台可以直接使用Module32First族函数遍历所有进程加载的模块。
HANDLE hSnapshot =CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
if (hSnapshot) {MODULEENTRY32 md32;md32.dwSize = sizeof(md32);if (Module32First(hSnapshot, &md32)) {do {std::string filepath;W2A(md32.szExePath, &filepath);ptable->push_back(filepath);} while (Module32Next(hSnapshot, &md32));}CloseHandle(hSnapshot);
}
5. CPU利用率
1)linux
进程的Cpu利用率无法直接从系统获取,而需要通过时间片算法来计算。在这里,我们参考top命令实现简单的进程Cpu利用率计算。
算法:
- 获取内核频率Hertz;
hertz_ = sysconf(_SC_CLK_TCK)
- 读取系统运行时间,计算与上次的运行时间的差值et;
FILE *fp = fopen("/proc/uptime", "r");if (fp) {char line[1024];fgets(line, sizeof(line), fp);fclose(fp);sscanf(line, "%lf", uptime);}
- 计算刷新频率Frame_etscale
float et = uptime_cur - uptime_save_;if (et < 0.01) {et = 0.005;}uptime_save_ = uptime_cur;frame_etscale_ = 100.0f / ((float)hertz_ * (float)et * 1);
- 遍历读取进程的用户态u_time和内核态时间s_time;
char stat_path[PROCPATHLEN];sprintf(stat_path, "/proc/%d/stat", pid);FILE *fp = fopen(stat_path, "r");if (fp) {char line[1024];if (fgets(line, sizeof(line), fp)) {int64_t u_time, s_time, wait_u_time, wait_s_time, start_time;sscanf(line,"%*d %*s %*c %*d %*d %*d %*d %*d ""%*lu %*lu %*lu %*lu %*lu""%llu %llu %llu %llu""%*ld %*ld ""%*d ""%*ld ""%llu ", /* start_time */&u_time, &s_time, &wait_u_time, &wait_s_time, &start_time);cpu_time->s_time = s_time;cpu_time->u_time = u_time;cpu_time->start_time = start_time;}fclose(fp);}
- 时间切片
- 重复步骤2、3、4,计算进程的CPU利用率cpu_usage ;
tics = process.new_time - process.old_time;cpu_usage = tics * etscale_;
2)Window
windows平台的算法与Linux类似。
算法:
- 获取当前CPU时间
FILETIME idleTime, kernelTime, userTime;if (!GetSystemTimes(&idleTime, &kernelTime, &userTime)) {return;}ULARGE_INTEGER cpu_time;cpu_time.LowPart = kernelTime.dwLowDateTime + userTime.dwLowDateTime;cpu_time.HighPart = kernelTime.dwHighDateTime + userTime.dwHighDateTime;time = cpu_time.QuadPart;
- 遍历所有进程的时间
HANDLE hProcess =OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);if (hProcess) {FILETIME start_time, exit_time, s_time, u_time;if (!GetProcessTimes(hProcess, &start_time, &exit_time, &s_time, &u_time)) {return false;}FileTimeToInt64(s_time, cpu_time->s_time);FileTimeToInt64(u_time, cpu_time->u_time);FileTimeToInt64(start_time, cpu_time->start_time);CloseHandle(hProcess);}
- 时间切片
- 重复步骤1、2,计算进程的CPU利用率cpu_usage ;
*cpu_usage = (process.new_time - process.old_time) * 1000 /(system_time_.new_time - system_time_.old_time);
6. 内存占用
1)linux
/proc/[PID]/status
:这个文件包含了有关进程状态的各种信息,如进程ID、父进程ID、运行状态、内存使用情况等。
// 获取物理内存总量,单位Byte
const char *meminfo_path = "/proc/meminfo";
FILE *fp = fopen(meminfo_path, "r");
if (fp) {char line[4096];while (fgets(line, sizeof(line), fp)) {if (strncmp(line, "MemTotal:", 9) == 0) {sscanf(line, "%*s:%d", sys_mem_size_);break;}}fclose(fp);
}//获取进程物理内存占用,单位KB
char status_path[PROCPATHLEN];
sprintf(status_path, "/proc/%d/status", pid);
FILE *fp = fopen(status_path, "r");
if (fp) {char line[4096];while (fgets(line, sizeof(line), fp)) {if (strncmp(line, "VmRSS:", 6) == 0) {sscanf(line, "%*s:%d", mem_used_size);break;}}fclose(fp);
}
2)Window
MEMORYSTATUSEX mem_info;
mem_info.dwLength = sizeof(mem_info);
// 获取物理内存总量,单位Byte
if (GlobalMemoryStatusEx(&mem_info)) {sys_mem_size_ = mem_info.ullTotalPhys;
}SYSTEM_INFO si;
GetSystemInfo(&si);
HANDLE hProcess =OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (hProcess) {PSAPI_WORKING_SET_INFORMATION workset_info = NULL;// 查询进程内存工具集if (!QueryWorkingSet(hProcess, &workset_info, sizeof(workset_info))) {if (GetLastError() == ERROR_BAD_LENGTH) {// 计算所需内存,由于工具集长度动态变化,因此在此多申请64个工具集空间。size_t length =sizeof(PSAPI_WORKING_SET_INFORMATION) +sizeof(PSAPI_WORKING_SET_BLOCK) * (workset_info.NumberOfEntries + 64);PPSAPI_WORKING_SET_INFORMATION p_workset_info =(PPSAPI_WORKING_SET_INFORMATION) new char[length];if (QueryWorkingSet(hProcess, p_workset_info, length)) {*mem_used_size = 0;for (int i = 0; i < p_workset_info->NumberOfEntries; i++) {// 判断工具集是否共享if (p_workset_info->WorkingSetInfo[i].Flags &&p_workset_info->WorkingSetInfo[i].Shared == 0) {*mem_used_size += si.dwPageSize;}}}delete[] (char *)p_workset_info;} else {return false;}}// Byte单位转换KB单位*mem_used_size = (*mem_used_size / 1024);return true;
}
7. 用户名
1)linux
/proc/[PID]/status
char status_path[PROCPATHLEN];
sprintf(status_path, "/proc/%d/status", pid);FILE *fp = fopen(status_path, "r");
if (fp) {char line[4096];while (fgets(line, sizeof(line), fp)) {if (strncmp(line, "Uid:", 4) == 0) {int32_t uid = 0;sscanf(line, "%*s:%d", &uid);struct passwd *_passwd;_passwd = getpwuid(uid);if (_passwd) {username = _passwd->pw_name;}}}fclose(fp);
}
2)Window
HANDLE handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
if (handle != 0x00) {HANDLE token;// 打开进程令牌if (OpenProcessToken(handle, TOKEN_QUERY, &token)) {DWORD token_size = 0;PTOKEN_USER p_token_user = NULL;SID_NAME_USE sn;// 从进程令牌中获取用户令牌if (!GetTokenInformation(token, TokenUser, p_token_user, token_size,&token_size)) {if (ERROR_INSUFFICIENT_BUFFER == GetLastError()) {p_token_user = (PTOKEN_USER)HeapAlloc(GetProcessHeap(), 0, token_size);if (p_token_user) {if (GetTokenInformation(token, TokenUser, p_token_user, token_size,&token_size)) {TCHAR szUserName[MAX_PATH] = {0};DWORD dwUserSize = MAX_PATH;TCHAR szDomain[MAX_PATH] = {0};DWORD dwDomainSize = MAX_PATH;// 根据用户令牌查询用户名if (LookupAccountSid(NULL, ((PTOKEN_USER)p_token_user)->User.Sid,szUserName, &dwUserSize, szDomain,&dwDomainSize, &sn)) {W2A(szUserName, username);ret = true;}}HeapFree(GetProcessHeap(), 0, p_token_user);}}}CloseHandle(token);}CloseHandle(handle);
}
8. 网络连接
1)linux
/proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6
: 提供了当前 TCP /TCP6/UDP/UDP6套接字的详细信息。
/proc/[PID]/fd/
:这是一个文件夹,包含了进程当前打开的文件描述符列表。
- 首先,解析套接字信息,包括套接字的inode号;
const char *tcp_file[] = {"/proc/net/tcp", "/proc/net/tcp6"};
for (int i = 0; i < 2; i++) {FILE *fp = fopen(tcp_file[i], "r");if (!fp) {return;}char line[1024];while (fgets(line, sizeof(line), fp)) {unsigned long rxq, txq, time_len, retr, inode;int num, local_port, remote_port, d, state, uid, timer_run, timeout;char rem_addr[128], local_addr[128];// 解析TCP信息num = sscanf(line,"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX ""%X:%lX %lX %d %d %lu %*s\n",&d, local_addr, &local_port, rem_addr, &remote_port, &state,&txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout,&inode);if (num < 11) {continue;}// ipv6地址if (strlen(local_addr) > 8) {char addr6[INET6_ADDRSTRLEN];struct in6_addr in6;sscanf(local_addr, "%08X%08X%08X%08X", &in6.s6_addr32[0],&in6.s6_addr32[1], &in6.s6_addr32[2], &in6.s6_addr32[3]);// 端口序转换inet_ntop(AF_INET6, &in6, addr6, sizeof(addr6));sscanf(rem_addr, "%08X%08X%08X%08X", &in6.s6_addr32[0], &in6.s6_addr32[1],&in6.s6_addr32[2], &in6.s6_addr32[3]);inet_ntop(AF_INET6, &in6, addr6, sizeof(addr6));} else {// ipv4地址char addr[INET_ADDRSTRLEN];struct in_addr in;sscanf(local_addr, "%X", &in.s_addr);inet_ntop(AF_INET, &in, addr, sizeof(addr));sscanf(rem_addr, "%X", &in.s_addr);inet_ntop(AF_INET, &in, addr, sizeof(addr));}}fclose(fp);
}const char *udp_file[] = {"/proc/net/udp", "/proc/net/udp6"};
for (int i = 0; i < 2; i++) {FILE *fp = fopen(udp_file[i], "r");if (!fp) {return false;}char line[1024];while (fgets(line, sizeof(line), fp)) {unsigned long rxq, txq, time_len, retr, inode;int num, local_port, remote_port, d, state, uid, timer_run, timeout;char rem_addr[128], local_addr[128];// 解析UDP信息num = sscanf(line,"%d: %64[0-9A-Fa-f]:%X %64[0-9A-Fa-f]:%X %X %lX:%lX ""%X:%lX %lX %d %d %lu %*s\n",&d, local_addr, &local_port, rem_addr, &remote_port, &state,&txq, &rxq, &timer_run, &time_len, &retr, &uid, &timeout,&inode);if (num < 10) {continue;}if (strlen(local_addr) > 8) {// ipv6地址char addr6[INET6_ADDRSTRLEN];struct in6_addr in6;sscanf(local_addr, "%08X%08X%08X%08X", &in6.s6_addr32[0],&in6.s6_addr32[1], &in6.s6_addr32[2], &in6.s6_addr32[3]);inet_ntop(AF_INET6, &in6, addr6, sizeof(addr6));} else {// ipv4地址char addr[INET_ADDRSTRLEN];struct in_addr in;sscanf(local_addr, "%X", &in.s_addr);inet_ntop(AF_INET, &in, addr, sizeof(addr));}}fclose(fp);
}
- 遍历所有进程的fd文件夹,匹配inode号。
2)WIndows
直接使用GetExtendedTcpTable和GetExtendedUdpTable函数查询TCP和UDP信息。
DWORD bufferSize;
MIB_TCPTABLE_OWNER_PID *net_table = NULL;WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData)) {return;
}do {SOCKADDR_IN v4 = {AF_INET};SOCKADDR_IN6 v6 = {AF_INET6};int32_t ip[] = {AF_INET6, AF_INET};char ipaddress[INET6_ADDRSTRLEN];DWORD ipaddress_length = 0;for (int i = 0; i < 2; i++) {bufferSize = 0;if (GetExtendedTcpTable(NULL, &bufferSize, FALSE, ip[i],TCP_TABLE_OWNER_PID_ALL,0) == ERROR_INSUFFICIENT_BUFFER) {BYTE *net_table = new BYTE[bufferSize];if (net_table != NULL) {if (GetExtendedTcpTable(net_table, &bufferSize, FALSE, ip[i],TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {if (i == 0) {MIB_TCP6TABLE_OWNER_PID *net_table_v6 =(MIB_TCP6TABLE_OWNER_PID *)net_table;for (DWORD i = 0; i < net_table_v6->dwNumEntries; i++) {MIB_TCP6ROW_OWNER_PID row = net_table_v6->table[i];// local addr infoipaddress_length = INET6_ADDRSTRLEN;memcpy(v6.sin6_addr.s6_addr, row.ucLocalAddr,sizeof(row.ucLocalAddr));if (!WSAAddressToStringA((LPSOCKADDR)&v6, sizeof(v6), NULL,ipaddress, &ipaddress_length)) {local_host = ipaddress;}local_port = ntohs(row.dwLocalPort);// remote addr infoipaddress_length = INET6_ADDRSTRLEN;memcpy(v6.sin6_addr.s6_addr, row.ucRemoteAddr,sizeof(row.ucRemoteAddr));if (!WSAAddressToStringA((LPSOCKADDR)&v6, sizeof(v6), NULL,ipaddress, &ipaddress_length)) {remote_host = ipaddress;}remote_port = ntohs(row.dwRemotePort);}} else {MIB_TCPTABLE_OWNER_PID *net_table_v4 =(MIB_TCPTABLE_OWNER_PID *)net_table;for (DWORD i = 0; i < net_table_v4->dwNumEntries; i++) {MIB_TCPROW_OWNER_PID row = net_table_v4->table[i];// local addr infoipaddress_length = INET_ADDRSTRLEN;v4.sin_addr.s_addr = row.dwLocalAddr;if (!WSAAddressToStringA((LPSOCKADDR)&v4, sizeof(v4), NULL,ipaddress, &ipaddress_length)) {local_host = ipaddress;}local_port = ntohs(row.dwLocalPort);// remote addr infoipaddress_length = INET_ADDRSTRLEN;v4.sin_addr.s_addr = row.dwRemoteAddr;if (!WSAAddressToStringA((LPSOCKADDR)&v4, sizeof(v4), NULL,ipaddress, &ipaddress_length)) {remote_host = ipaddress;}remote_port = ntohs(row.dwRemotePort);}}}delete[] net_table;}}}for (int i = 0; i < 2; i++) {bufferSize = 0;if (GetExtendedUdpTable(NULL, &bufferSize, FALSE, ip[i],UDP_TABLE_OWNER_PID,0) == ERROR_INSUFFICIENT_BUFFER) {BYTE *net_table = new BYTE[bufferSize];if (net_table != NULL) {if (GetExtendedUdpTable(net_table, &bufferSize, FALSE, ip[i],UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {if (i == 0) {MIB_UDP6TABLE_OWNER_PID *net_table_v6 =(MIB_UDP6TABLE_OWNER_PID *)net_table;for (DWORD i = 0; i < net_table_v6->dwNumEntries; i++) {MIB_UDP6ROW_OWNER_PID row = net_table_v6->table[i];// local addr netInfoipaddress_length = INET6_ADDRSTRLEN;memcpy(v6.sin6_addr.s6_addr, row.ucLocalAddr,sizeof(row.ucLocalAddr));if (!WSAAddressToStringA((LPSOCKADDR)&v6, sizeof(v6), NULL,ipaddress, &ipaddress_length)) {local_host = ipaddress;}local_port = ntohs(row.dwLocalPort);}} else {MIB_UDPTABLE_OWNER_PID *net_table_v4 =(MIB_UDPTABLE_OWNER_PID *)net_table;for (DWORD i = 0; i < net_table_v4->dwNumEntries; i++) {MIB_UDPROW_OWNER_PID row = net_table_v4->table[i];// local addr netInfoipaddress_length = INET_ADDRSTRLEN;v4.sin_addr.s_addr = row.dwLocalAddr;if (!WSAAddressToStringA((LPSOCKADDR)&v4, sizeof(v4), NULL,ipaddress, &ipaddress_length)) {local_host = ipaddress;}local_port = ntohs(row.dwLocalPort);}}}delete[] net_table;}}}
} while (false);
WSACleanup();
9. 待续
……