IOException : 읽기 실패, 소켓이 닫힐 수 있음 -Android 4.3 Bluetooth
현재 Android 4.3 (Build JWR66Y, 두 번째 4.3 업데이트)을 사용하여 Nexus 7 (2012)에서 BluetoothSocket을 열 때 이상한 예외를 처리합니다. 일부 관련 게시물 (예 : https://stackoverflow.com/questions/13648373/bluetoothsocket-connect-throwing-exception-read-failed )을 감사지만이 문제에 대한 해결 방법을 제공하는 것은 아닙니다. 멍청한 루프를 사용해도 효과가 없었습니다.
역할 장치 ( http://images04.olx.com/ui/15/53/76/1316534072_254254776_2-OBD-II-BLUTOOTH-ADAPTERSCLEAR-CHECK-ENGINE- 와 이름 없음 OBD-II 자동차 어댑터)를 다루고 있습니다. LIGHTS-WITH-YOUR-PHONE-Oceanside.jpg ). 내 Android 2.3.7 전화는 연결 문제가 동료의 Xperia (Android 4.1.2) 작동합니다. 다른 Google Nexus ( 'One'또는 'S'인지 모르겠지만 '4'는 아님)도 Android 4.3에서 실패합니다.
다음은 연결 설정의 스 니펫입니다. 서비스 내에서 생성 된 자체 실행 실행됩니다.
private class ConnectThread extends Thread {
private static final UUID EMBEDDED_BOARD_SPP = UUID
.fromString("00001101-0000-1000-8000-00805F9B34FB");
private BluetoothAdapter adapter;
private boolean secure;
private BluetoothDevice device;
private List<UUID> uuidCandidates;
private int candidate;
protected boolean started;
public ConnectThread(BluetoothDevice device, boolean secure) {
logger.info("initiliasing connection to device "+device.getName() +" / "+ device.getAddress());
adapter = BluetoothAdapter.getDefaultAdapter();
this.secure = secure;
this.device = device;
setName("BluetoothConnectThread");
if (!startQueryingForUUIDs()) {
this.uuidCandidates = Collections.singletonList(EMBEDDED_BOARD_SPP);
this.start();
} else{
logger.info("Using UUID discovery mechanism.");
}
/*
* it will start upon the broadcast receive otherwise
*/
}
private boolean startQueryingForUUIDs() {
Class<?> cl = BluetoothDevice.class;
Class<?>[] par = {};
Method fetchUuidsWithSdpMethod;
try {
fetchUuidsWithSdpMethod = cl.getMethod("fetchUuidsWithSdp", par);
} catch (NoSuchMethodException e) {
logger.warn(e.getMessage());
return false;
}
Object[] args = {};
try {
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
BluetoothDevice deviceExtra = intent.getParcelableExtra("android.bluetooth.device.extra.DEVICE");
Parcelable[] uuidExtra = intent.getParcelableArrayExtra("android.bluetooth.device.extra.UUID");
uuidCandidates = new ArrayList<UUID>();
for (Parcelable uuid : uuidExtra) {
uuidCandidates.add(UUID.fromString(uuid.toString()));
}
synchronized (ConnectThread.this) {
if (!ConnectThread.this.started) {
ConnectThread.this.start();
ConnectThread.this.started = true;
unregisterReceiver(this);
}
}
}
};
registerReceiver(receiver, new IntentFilter("android.bleutooth.device.action.UUID"));
registerReceiver(receiver, new IntentFilter("android.bluetooth.device.action.UUID"));
fetchUuidsWithSdpMethod.invoke(device, args);
} catch (IllegalArgumentException e) {
logger.warn(e.getMessage());
return false;
} catch (IllegalAccessException e) {
logger.warn(e.getMessage());
return false;
} catch (InvocationTargetException e) {
logger.warn(e.getMessage());
return false;
}
return true;
}
public void run() {
boolean success = false;
while (selectSocket()) {
if (bluetoothSocket == null) {
logger.warn("Socket is null! Cancelling!");
deviceDisconnected();
openTroubleshootingActivity(TroubleshootingActivity.BLUETOOTH_EXCEPTION);
}
// Always cancel discovery because it will slow down a connection
adapter.cancelDiscovery();
// Make a connection to the BluetoothSocket
try {
// This is a blocking call and will only return on a
// successful connection or an exception
bluetoothSocket.connect();
success = true;
break;
} catch (IOException e) {
// Close the socket
try {
shutdownSocket();
} catch (IOException e2) {
logger.warn(e2.getMessage(), e2);
}
}
}
if (success) {
deviceConnected();
} else {
deviceDisconnected();
openTroubleshootingActivity(TroubleshootingActivity.BLUETOOTH_EXCEPTION);
}
}
private boolean selectSocket() {
if (candidate >= uuidCandidates.size()) {
return false;
}
BluetoothSocket tmp;
UUID uuid = uuidCandidates.get(candidate++);
logger.info("Attempting to connect to SDP "+ uuid);
try {
if (secure) {
tmp = device.createRfcommSocketToServiceRecord(
uuid);
} else {
tmp = device.createInsecureRfcommSocketToServiceRecord(
uuid);
}
bluetoothSocket = tmp;
return true;
} catch (IOException e) {
logger.warn(e.getMessage() ,e);
}
return false;
}
}
에서 코드가 실패 bluetoothSocket.connect()
합니다. 나는 java.io.IOException: read failed, socket might closed, read ret: -1
. 이 GitHub의 해당 소스입니다 : https://github.com/android/platform_frameworks_base/blob/android-4.3_r2/core/java/android/bluetooth/BluetoothSocket.java#L504 https 에서 호출되는 readInt ()를 호출을 통해 . : //github.com/android/platform_frameworks_base/blob/android-4.3_r2/core/java/android/bluetooth/BluetoothSocket.java#L319
사용 된 소켓의 일부 메타 데이터로 인해 다음 정보가 생성됩니다. Nexus 7과 2.3.7 휴대 전화에서 정확히 동일합니다.
Bluetooth Device 'OBDII'
Address: 11:22:33:DD:EE:FF
Bond state: 12 (bonded)
Type: 1
Class major version: 7936
Class minor version: 7936
Class Contents: 0
Contents: 0
다른 OBD-II 어댑터 (더 확장 성)가 모두 작동합니다. 내가 뭔가를 놓치고 있거나 Android의 버그 일 가능성이 있습니까?
마침내 해결 방법을 찾았습니다. 마법은 BluetoothDevice
클래스 내부에 숨겨져 있습니다 ( https://github.com/android/platform_frameworks_base/blob/android-4.3_r2/core/java/android/bluetooth/BluetoothDevice.java#L1037 참조 ).
이제 예외를 밖면 아래 BluetoothSocket
소스 코드와 함께 대체를 인스턴스화 합니다. 보시다시피 createRfcommSocket
반사를 통해 숨겨진 메서드 를 호출합니다 . 이 방법이 왜 숨겨져 있는지 전혀 모르겠습니다. 소스 코드는 그것을 public
마치 ...
Class<?> clazz = tmp.getRemoteDevice().getClass();
Class<?>[] paramTypes = new Class<?>[] {Integer.TYPE};
Method m = clazz.getMethod("createRfcommSocket", paramTypes);
Object[] params = new Object[] {Integer.valueOf(1)};
fallbackSocket = (BluetoothSocket) m.invoke(tmp.getRemoteDevice(), params);
fallbackSocket.connect();
connect()
더 이상 실패하지 않습니다. 여전히 몇 가지 문제가 발생했습니다. 기본적으로 이것은 차단되고 실패합니다. SPP- 장치를 사용하면 도움이됩니다. 이미 connect()
연결되어있는 경우 에도 요청을받습니다 .
최신 정보 :
다음은 일부 중첩 된 클래스를 포함하는 완전한 클래스입니다. 실제 구현을 위해 내부의 클래스로 보유 할 수 있습니다.
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.util.List;
import java.util.UUID;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
public class BluetoothConnector {
private BluetoothSocketWrapper bluetoothSocket;
private BluetoothDevice device;
private boolean secure;
private BluetoothAdapter adapter;
private List<UUID> uuidCandidates;
private int candidate;
/**
* @param device the device
* @param secure if connection should be done via a secure socket
* @param adapter the Android BT adapter
* @param uuidCandidates a list of UUIDs. if null or empty, the Serial PP id is used
*/
public BluetoothConnector(BluetoothDevice device, boolean secure, BluetoothAdapter adapter,
List<UUID> uuidCandidates) {
this.device = device;
this.secure = secure;
this.adapter = adapter;
this.uuidCandidates = uuidCandidates;
if (this.uuidCandidates == null || this.uuidCandidates.isEmpty()) {
this.uuidCandidates = new ArrayList<UUID>();
this.uuidCandidates.add(UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"));
}
}
public BluetoothSocketWrapper connect() throws IOException {
boolean success = false;
while (selectSocket()) {
adapter.cancelDiscovery();
try {
bluetoothSocket.connect();
success = true;
break;
} catch (IOException e) {
//try the fallback
try {
bluetoothSocket = new FallbackBluetoothSocket(bluetoothSocket.getUnderlyingSocket());
Thread.sleep(500);
bluetoothSocket.connect();
success = true;
break;
} catch (FallbackException e1) {
Log.w("BT", "Could not initialize FallbackBluetoothSocket classes.", e);
} catch (InterruptedException e1) {
Log.w("BT", e1.getMessage(), e1);
} catch (IOException e1) {
Log.w("BT", "Fallback failed. Cancelling.", e1);
}
}
}
if (!success) {
throw new IOException("Could not connect to device: "+ device.getAddress());
}
return bluetoothSocket;
}
private boolean selectSocket() throws IOException {
if (candidate >= uuidCandidates.size()) {
return false;
}
BluetoothSocket tmp;
UUID uuid = uuidCandidates.get(candidate++);
Log.i("BT", "Attempting to connect to Protocol: "+ uuid);
if (secure) {
tmp = device.createRfcommSocketToServiceRecord(uuid);
} else {
tmp = device.createInsecureRfcommSocketToServiceRecord(uuid);
}
bluetoothSocket = new NativeBluetoothSocket(tmp);
return true;
}
public static interface BluetoothSocketWrapper {
InputStream getInputStream() throws IOException;
OutputStream getOutputStream() throws IOException;
String getRemoteDeviceName();
void connect() throws IOException;
String getRemoteDeviceAddress();
void close() throws IOException;
BluetoothSocket getUnderlyingSocket();
}
public static class NativeBluetoothSocket implements BluetoothSocketWrapper {
private BluetoothSocket socket;
public NativeBluetoothSocket(BluetoothSocket tmp) {
this.socket = tmp;
}
@Override
public InputStream getInputStream() throws IOException {
return socket.getInputStream();
}
@Override
public OutputStream getOutputStream() throws IOException {
return socket.getOutputStream();
}
@Override
public String getRemoteDeviceName() {
return socket.getRemoteDevice().getName();
}
@Override
public void connect() throws IOException {
socket.connect();
}
@Override
public String getRemoteDeviceAddress() {
return socket.getRemoteDevice().getAddress();
}
@Override
public void close() throws IOException {
socket.close();
}
@Override
public BluetoothSocket getUnderlyingSocket() {
return socket;
}
}
public class FallbackBluetoothSocket extends NativeBluetoothSocket {
private BluetoothSocket fallbackSocket;
public FallbackBluetoothSocket(BluetoothSocket tmp) throws FallbackException {
super(tmp);
try
{
Class<?> clazz = tmp.getRemoteDevice().getClass();
Class<?>[] paramTypes = new Class<?>[] {Integer.TYPE};
Method m = clazz.getMethod("createRfcommSocket", paramTypes);
Object[] params = new Object[] {Integer.valueOf(1)};
fallbackSocket = (BluetoothSocket) m.invoke(tmp.getRemoteDevice(), params);
}
catch (Exception e)
{
throw new FallbackException(e);
}
}
@Override
public InputStream getInputStream() throws IOException {
return fallbackSocket.getInputStream();
}
@Override
public OutputStream getOutputStream() throws IOException {
return fallbackSocket.getOutputStream();
}
@Override
public void connect() throws IOException {
fallbackSocket.connect();
}
@Override
public void close() throws IOException {
fallbackSocket.close();
}
}
public static class FallbackException extends Exception {
/**
*
*/
private static final long serialVersionUID = 1L;
public FallbackException(Exception e) {
super(e);
}
}
}
글쎄, 나는 내 코드에 동일한 문제가있는 것이 안드로이드 4.2 블루투스 스택 변경이 있기 때문입니다. 그래서 내 코드는 android <4.2 인 장치에서 잘 실행되고 다른 장치에서는 "읽기 실패, 소켓이 닫혔거나 시간 초과, 읽기 ret : -1"라는 유명한 예외가 발생 했습니다.
socket.mPort
매개 변수에 문제가 있습니다. 당신이 사용하여 소켓을 만드는 경우 socket = device.createRfcommSocketToServiceRecord(SERIAL_UUID);
는이 mPort
값의 정수 "얻는다 -1 ", 및 "로 설정해야합니다. 값은 4.2> = 안드로이드에 대한 작업을하지 않는 것 같다 (1) ". 나쁜 소식은 createRfcommSocketToServiceRecord
UUID 만 매개 변수로 받아들이고 mPort
다른 aproach를 만나는 것입니다. @matthes가 게시 한 답변 도 저에게 이지만 단순화했습니다 socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);
.. 두 번째 소켓 속성을 폴백으로 매 깁니다.
따라서 코드는 다음과 가변됩니다 (ELM327 장치의 SPP에 연결).
BluetoothAdapter btAdapter = BluetoothAdapter.getDefaultAdapter();
if (btAdapter.isEnabled()) {
SharedPreferences prefs_btdev = getSharedPreferences("btdev", 0);
String btdevaddr=prefs_btdev.getString("btdevaddr","?");
if (btdevaddr != "?")
{
BluetoothDevice device = btAdapter.getRemoteDevice(btdevaddr);
UUID SERIAL_UUID = UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"); // bluetooth serial port service
//UUID SERIAL_UUID = device.getUuids()[0].getUuid(); //if you don't know the UUID of the bluetooth device service, you can get it like this from android cache
BluetoothSocket socket = null;
try {
socket = device.createRfcommSocketToServiceRecord(SERIAL_UUID);
} catch (Exception e) {Log.e("","Error creating socket");}
try {
socket.connect();
Log.e("","Connected");
} catch (IOException e) {
Log.e("",e.getMessage());
try {
Log.e("","trying fallback...");
socket =(BluetoothSocket) device.getClass().getMethod("createRfcommSocket", new Class[] {int.class}).invoke(device,1);
socket.connect();
Log.e("","Connected");
}
catch (Exception e2) {
Log.e("", "Couldn't establish Bluetooth connection!");
}
}
}
else
{
Log.e("","BT device not selected");
}
}
먼저 블루투스 2.x 장치와 통신해야하는 경우이 문서에 다음과 같이 명시되어 있습니다.
힌트 : Bluetooth 내장 보드에 연결하는 경우 잘 B34 SPP UUID 00001101-0000-1000-8000-00805F9FB 를 선택합니다 . 그러나 Android 피어에 연결하는 경우 고유 한 UUID를 생성하십시오.
나는 그것이 작동 할 것이라고 생각하지 않았지만 UUID를 00001101-0000-1000-8000-00805F9B34FB
그것 으로 교체 해야만 작동합니다. 그러나이 코드는 SDK 버전 문제를 처리하는 무시하고 다음과 같은 메소드를 정의한 후 함수 device.createRfcommSocketToServiceRecord(mMyUuid);
를 tmp = createBluetoothSocket(mmDevice);
다음으로 대체 할 수 있습니다 .
private BluetoothSocket createBluetoothSocket(BluetoothDevice device)
throws IOException {
if(Build.VERSION.SDK_INT >= 10){
try {
final Method m = device.getClass().getMethod("createInsecureRfcommSocketToServiceRecord", new Class[] { UUID.class });
return (BluetoothSocket) m.invoke(device, mMyUuid);
} catch (Exception e) {
Log.e(TAG, "Could not create Insecure RFComm Connection",e);
}
}
return device.createRfcommSocketToServiceRecord(mMyUuid);
}
소스 코드는 내 것이이 웹 사이트 에서 물건 것 입니다.
여기에 설명 된 것과 같은 것과 같은 것입니다. "소켓이 닫힘"으로 "소켓이 닫힘"으로 있었지만 이후 연결은 실패했습니다.
여기에 설명 된 해결 방법이 있다는 것이 조금 이상하다는 것을 알았습니다. 내 코드를 살펴본 후 소켓의 InputStream 및 OutputSteram을 닫는 것을 잊고 ConnectedThreads를 제대로 종료하지 않아 갑자기 소리를 발견했습니다.
내가 사용하는 ConnectedThread는 여기 예제와 동일합니다.
http://developer.android.com/guide/topics/connectivity/bluetooth.html
ConnectThread와 ConnectedThread는 서로 다른 두 클래스입니다.
ConnectedThread를 시작하는 모든 클래스는 프로그램에서 인터럽트 () 및 취소 ()를 호출해야합니다. ConnectedTread.cancel () 메서드에 mmInStream.close () 및 mmOutStream.close ()를 추가했습니다.
정식 / 스트림 / 소켓을 제대로 닫은 후 문제없이 새 소켓을 만들 수 있습니다.
글쎄, 나는 실제로 문제를 발견했습니다.
를 사용하여 연결을 시도하는 대부분의 사람들은 socket.Connect();
라는 예외 를 받습니다 Java.IO.IOException: read failed, socket might closed, read ret: -1
.
경우에 따라 Bluetooth 장치에 따라 달라집니다. BLE (저에너지)와 클래식이라는 두 가지 유형의 Bluetooth가 있기 때문입니다.
블루투스 장치 유형을 확인 후 다음 코드를 참조하십시오.
String checkType;
var listDevices = BluetoothAdapter.BondedDevices;
if (listDevices.Count > 0)
{
foreach (var btDevice in listDevices)
{
if(btDevice.Name == "MOCUTE-032_B52-CA7E")
{
checkType = btDevice.Type.ToString();
Console.WriteLine(checkType);
}
}
}
며칠 동안 문제를 해결하려고 노력했지만 오늘부터 문제를 발견했습니다. @matthes의 솔루션에는 그가 이미 말했듯이 불행히도 여전히 몇 가지 문제가 해결 여기에 내 솔루션이 있습니다.
현재 Xamarin Android에서 작업하지만 다른 플랫폼 작동합니다.
해결책
두 개 이상인 경우에는 다른 장치를 제거합니다. 따라서 연결하려는 하나만 유지하십시오 (오른쪽 이미지 참조).
"MOCUTE-032_B52-CA7E"와 "Blue Easy"라는 두 개의 이미지가 있습니다. 그게 문제지만 왜 그 문제가 발생하는지 모르겠습니다. 블루투스 프로토콜이 다른 블루투스 장치에서 정보를 얻을 수 있습니다.
그러나 socket.Connect();
지금은 아무 문제없이 훌륭하게 작동합니다. 그래서 저는 이것을 공유하고 싶었습니다. 그 오류가 정말 짜증나 기 때문입니다.
행운을 빕니다!
당신은 registerReceiver(receiver, new IntentFilter("android.bleutooth.device.action.UUID"));
"bluetooth"철자가 "bleutooth"로 넣습니다.
최신 버전의 Android에서는 소켓에 연결하려고 할 때 어댑터가 여전히 발견되었습니다. Bluetooth 어댑터에서 cancelDiscovery 메서드를 호출했지만 BluetoothAdapter.ACTION_DISCOVERY_FINISHED 작업으로 BroadcastReceiver의 onReceive () 메서드에 대한 호출이 될 때까지 기다려야했습니다.
어댑터가 검색을 중지 할 때까지 기다린 후 소켓의 연결 호출이 성공했습니다.
누군가 Kotlin에 문제가있는 경우 허용 된 답변을 몇 가지 변형으로 따라야했습니다.
fun print(view: View, text: String) {
var adapter = BluetoothAdapter.getDefaultAdapter();
var pairedDevices = adapter.getBondedDevices()
var uuid = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
if (pairedDevices.size > 0) {
for (device in pairedDevices) {
var s = device.name
if (device.getName().equals(printerName, ignoreCase = true)) {
Thread {
var socket = device.createInsecureRfcommSocketToServiceRecord(uuid)
var clazz = socket.remoteDevice.javaClass
var paramTypes = arrayOf<Class<*>>(Integer.TYPE)
var m = clazz.getMethod("createRfcommSocket", *paramTypes)
var fallbackSocket = m.invoke(socket.remoteDevice, Integer.valueOf(1)) as BluetoothSocket
try {
fallbackSocket.connect()
var stream = fallbackSocket.outputStream
stream.write(text.toByteArray(Charset.forName("UTF-8")))
} catch (e: Exception) {
e.printStackTrace()
Snackbar.make(view, "An error occurred", Snackbar.LENGTH_SHORT).show()
}
}.start()
}
}
}
}
도움이되기를 바랍니다.
블루투스 장치는 클래식 모드와 LE 모드에서 동시에 작동 할 수 있습니다. 연결하는 방법에 따라 다른 MAC 주소를 사용합니다. 통화 socket.connect()
는 Bluetooth Classic을 사용하여 스캔 할 때받은 장치가 실제로 클래식 장치인지 확인해야합니다.
그러나 클래식 장치 만 필터링하는 것이지만.
if(BluetoothDevice.DEVICE_TYPE_LE == device.getType()){ //socket.connect() }
이 검사가 하이브리드 스캔이 클래식 장치 또는 BLE 장치를 먼저 제공할지 여부에 대한 경쟁 조건입니다. 특정 장치가 안정된 것처럼 보이게 할 수 있습니다.
나는 또한이 문제에 직면하여 앞에서 언급했듯이 리플렉션을 사용하여 소켓을 만드는 두 가지 방법으로 수 있습니다. 두 번째는 클라이언트가 주어진 UUID로 서버를 찾고 서버가 클라이언트와 송신되지 않는 경우 발생합니다. 주어진 클라이언트 UUID로 서버를 만든 다음 서버 측에서 클라이언트를 수신하고 수락하면 작동합니다.
이 문제가 발생하기 전에 소켓을 입력 및 출력 스트림을 닫아 수정했습니다. 이제 연결을 끄고 문제없이 다시 수 있습니다.
https://stackoverflow.com/a/3039807/5688612
Kotlin에서 :
fun disconnect() {
bluetoothSocket.inputStream.close()
bluetoothSocket.outputStream.close()
bluetoothSocket.close()
}
나는 같은 문제가 있었지만 결국 내 문제를 이해하고 (범위를 벗어난) Bluetooth 적용 범위에서 연결되었습니다.
코드의 다른 부분이 이미 소켓 및 UUID를 사용하여 연결 한 경우이 오류가 발생합니다.
필터 동작을 추가하여 문제가 해결되었습니다.
// Register for broadcasts when a device is discovered
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(BluetoothDevice.ACTION_FOUND);
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_STARTED);
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED);
registerReceiver(mReceiver, intentFilter);
같은 저도 IOException
을 받았지만 안드로이드 시스템 데모 인 "BluetoothChat"프로젝트가 작동했습니다. 문제가 UUID라고 판단했습니다.
난 내 대체 그래서 UUID.fromString("00001001-0000-1000-8000-00805F9B34FB")
로를 UUID.fromString("8ce255c0-200a-11e0-ac64-0800200c9a66")
대부분의 장면에서 열심히 만하고 블루투스 장치를 다시 시작해야합니다.
'IT' 카테고리의 다른 글
명령 줄에서 nvidia 드라이버 버전을 얻는 방법은 무엇입니까? (0) | 2020.08.31 |
---|---|
Google지도에서 한 지점 주변의 반경 그리기 (0) | 2020.08.31 |
양식의 clean () 메소드에서 요청 또는 다른 변수에 어떻게 액세스합니까? (0) | 2020.08.31 |
JavaScript에서 파일 열기 대화 상자 (0) | 2020.08.31 |
첫 번째 호출은 실패하고 두 번째 호출은 성공하는 시뮬레이션 (0) | 2020.08.31 |