'APNS'에 해당되는 글 2건

Push Notification

개발/Note 2023. 12. 27. 16:42
반응형

python으로 간단한 모바일 푸시 서버를 만들어 보았다.

예약 발송 및 batch 처리등 처리 하였다. 

 

#
# push_server.py
# APNs, FCM, 예약 및 batch 푸시 발송
#

import json
import jwt
import time
from datetime import datetime
from hyper import HTTPConnection
from http.client import HTTPSConnection
from apscheduler.schedulers.blocking import BlockingScheduler
import mysql.connector
import configparser
from apscheduler.events import EVENT_JOB_EXECUTED

# Read configuration from config.ini
config = configparser.ConfigParser()
config.read('config.ini')

# SSL/TLS (Secure Sockets Layer / Transport Layer Security)
CA_FILE = config.get('SSL', 'ca_file', fallback=None)
CERT_FILE = config.get('SSL', 'cert_file', fallback=None)
KEY_FILE = config.get('SSL', 'key_file', fallback=None)

# Database
DB_HOST = config.get('Database', 'host')
DB_USER = config.get('Database', 'user')
DB_PASSWORD = config.get('Database', 'password')
DB_NAME = config.get('Database', 'database')
DB_PORT = config.get('Database', 'port')

# Protocol
PROTOCOL = config.get('Server', 'protocol', fallback='HTTP')

# APNs status codes
status_codes = {
    200: "Success",
    400: "Bad request",
    403: "Error with certificate or provider authentication token",
    405: "Invalid request method. Only POST requests are supported.",
    410: "Device token is no longer active for the topic.",
    413: "Notification payload was too large.",
    429: "Too many requests for the same device token.",
    500: "Internal server error",
    503: "Server is shutting down and unavailable.",
}

conn_class = HTTPConnection if PROTOCOL.upper() == 'HTTP' else HTTPSConnection

scheduler = BlockingScheduler()


# Create the request headers with expiration time
def create_request_headers(section_name, remaining_seconds):
    push_type = config.get(section_name, 'push_type')
    
    if push_type == "APNs": 
        return create_apns_request_headers(section_name, remaining_seconds)
    elif push_type == "FCM":
        return create_fcm_request_headers(section_name)
    else:
        return None

# Create APNs request headers
def create_apns_request_headers(section_name, remaining_seconds):
    auth_key_path = config.get(section_name, 'auth_key_path')
    team_id = config.get(section_name, 'team_id')
    key_id = config.get(section_name, 'key_id')
    bundle_id = config.get(section_name, 'bundle_id')
    
    with open(auth_key_path) as f:
        secret = f.read()
    
    token = jwt.encode(
        {
            'iss': team_id,
            'iat': time.time(),
        },
        secret,
        algorithm='ES256',
        headers={
            'alg': 'ES256',
            'kid': key_id,
        }
    )
    token_bytes = token.encode('ascii')
    
    headers = {
        'apns-priority': '10',
        'apns-topic': bundle_id,
        'authorization': 'bearer {0}'.format(token_bytes.decode('ascii'))
    }
    
    if remaining_seconds is not None:
        headers['apns-expiration'] = str(remaining_seconds)

    return headers

# Create FCM request headers
def create_fcm_request_headers(section_name):
    api_key = config.get(section_name, 'api_key') 
    
    headers = {
        'Authorization': 'key=' + api_key,
        'Content-Type': 'application/json'
    }
    return headers

# Function to send notification with scheduled time
def send_notification(conn, section_name, push_token, message, badge_count=0, remaining_seconds=0):
    request_headers = create_request_headers(section_name, remaining_seconds)

    push_type = config.get(section_name, 'push_type')

    payload_data = create_payload_data(push_type, message, badge_count)
    payload = json.dumps(payload_data).encode('utf-8')

    if push_type == "APNs": 
        conn.request('POST', f'/3/device/{push_token}', payload, headers=request_headers)
    elif push_type == "FCM":
        conn.request('POST', '/fcm/send', payload, headers=request_headers)
    
    resp = conn.get_response()

    status_code = resp.status
    description = status_codes.get(status_code, "Unknown status code")
    print(f"Status code: {status_code} - Description: {description}")
    print(resp.read())

# Create payload data based on push type
def create_payload_data(push_type, message, badge_count):
    if push_type == "APNs":
        return {
            'aps': {
                'alert': message,
                'badge': badge_count,
            }
        }
    elif push_type == "FCM":
        return {
            'to': push_token,
            'notification': {
                'title': '',
                'body': message,
            },
            'data': {
                'badge': badge_count,
            },
        }

# Function to send push with scheduled time
def send_push(conn, section_name, push_token, message, badge_count, scheduled_time, remaining_seconds=0):
    scheduler.add_listener(lambda event: scheduler.shutdown(wait=False), EVENT_JOB_EXECUTED)
    scheduler.add_job(
        send_notification,
        args=(conn, section_name, push_token, message, badge_count, remaining_seconds),
        trigger='date',
        run_date=datetime.fromtimestamp(scheduled_time)
    )
    scheduler.start()

# Function to send bulk push
def send_bulk_push(conn, section_name, users, message, badge_count, scheduled_time, remaining_seconds=0):
    scheduler.add_listener(lambda event: scheduler.shutdown(wait=False), EVENT_JOB_EXECUTED)
    for user in users: 
        push_token = user[6]
        scheduler.add_job(
            send_notification,
            args=(conn, section_name, push_token, message, badge_count, remaining_seconds),
            trigger='date',
            run_date=datetime.fromtimestamp(scheduled_time)
        )
    scheduler.start()

# Function to send push to users in batches
def send_push_to_users_in_batches(section_name, users, batch_size, message, badge_count, scheduled_time, remaining_seconds=0): 
    push_type = config.get(section_name, 'push_type')
    environment = config.get(section_name, 'environment', fallback='development')

    if push_type == "APNs": 
        push_url = 'api.development.push.apple.com' if environment == 'development' else 'api.push.apple.com'
    elif push_type == "FCM":
        push_url = 'fcm.googleapis.com'

    conn = conn_class(push_url + ':443', ca_certs=CA_FILE, cert_file=CERT_FILE, key_file=KEY_FILE)

    total_users = len(users)
    num_batches = (total_users + batch_size - 1) // batch_size
    for batch_number in range(num_batches):
        start_index = batch_number * batch_size
        end_index = (batch_number + 1) * batch_size
        batch_users = users[start_index:end_index]
        send_bulk_push(conn, section_name, batch_users, message, badge_count, scheduled_time, remaining_seconds)

    conn.close()

# MySQL connection and get user info
def get_users_from_database():
    connection_params = {
        'host': DB_HOST,
        'user': DB_USER,
        'password': DB_PASSWORD,
        'database': DB_NAME,
        'port': DB_PORT,
    }
    
    if PROTOCOL == 'HTTPS':
        connection_params['ssl'] = {
            'ca': CA_FILE,
            'cert': CERT_FILE,
            'key': KEY_FILE
        }
    
    connection = mysql.connector.connect(**connection_params)
    
    try:
        cursor = connection.cursor()
        query = 'SELECT * FROM users'
        cursor.execute(query)
        users = cursor.fetchall()
        return users
    finally:
        cursor.close()
        connection.close()

# Calculate the scheduled time (in seconds from now)
def get_push_schedule_time(year=None, month=None, day=None, hour=None, minute=None):
    current_time = datetime.now()

    if all(x is None for x in [year, month, day, hour, minute]):
        return int(current_time.timestamp())

    scheduled_time = datetime(
        year if year is not None else current_time.year,
        month if month is not None else current_time.month,
        day if day is not None else current_time.day,
        hour if hour is not None else current_time.hour,
        minute if minute is not None else current_time.minute
    )

    if scheduled_time < current_time:
        return int(current_time.timestamp())

    return int(scheduled_time.timestamp())

# Calculate the remaining expiration time (in seconds from now)
def get_remaining_expiry_seconds(year=None, month=None, day=None, hour=None, minute=None):
    current_time = datetime.now()
    expiration_time = datetime(
        year if year is not None else current_time.year,
        month if month is not None else current_time.month,
        day if day is not None else current_time.day,
        hour if hour is not None else current_time.hour,
        minute if minute is not None else current_time.minute
    )
    time_difference = expiration_time - current_time
    remaining_seconds = max(int(time_difference.total_seconds()), 0)
    return remaining_seconds




# Push scheduling time
scheduled_time = get_push_schedule_time(2023, 12, 19, 10, 37)
# Remaining seconds until push expiration time
remaining_seconds = get_remaining_expiry_seconds(2023, 12, 19, 10, 38)
print(f"Scheduled Time: {scheduled_time}")
print(f"Remaining Seconds: {remaining_seconds}")

# Section name (company name)
section_name = 'MyApp'
# Get user information
users_data = get_users_from_database()
# Batch size
batch_size = 1000
# Push message
message = "test message."
# Badge count
badge_count = 1

# Send push to users based on batch size
send_push_to_users_in_batches(section_name, users_data, batch_size, message, badge_count, scheduled_time, remaining_seconds)

print("Complete.")

 

#
# config.ini
#

[Server]
# HTTP 또는 HTTPS
protocol = HTTP

[SSL]
ca_file = cert/ca.pem
cert_file = cert/cert.pem
key_file = cert/key.pem

[Database]
host = 127.0.0.1
user = netcanis
password = 12345678
database = My_DB_Name
port = 3306

[HarexApp]
# APNs, FCM
push_type = APNs
# development 또는 production
environment = development
bundle_id = com.mycompany.test
auth_key_path = cert/mycompany/AuthKey.p8
key_id = XX8UHXF9XX
team_id = XX6KEXF2XX

[HarexApp2]
# APNs, FCM
push_type = FCM
# development 또는 production
environment = development
bundle_id = com.mycompany.test
api_key = XXXX.....XXXX

 

2024.01.11 - [Note] - BLE, Beacon, iBeacon

2024.01.11 - [Note] - BLE Advertising Payload format 샘플 분석

2024.01.09 - [Note] - Packet Format for the LE Uncoded PHYs

2024.01.04 - [iOS] - Floating, Dragging Button

2023.12.27 - [생각의 조각들] - 말이 통하지 않는 사람과 싸우지 말라.

2023.12.27 - [Server] - Push Notification

2023.12.27 - [AI,ML,ALGORITHM] - A star

2023.12.07 - [iOS] - XOR 연산을 이용한 문자열 비교

2023.11.03 - [AI,ML,ALGORITHM] - Gomoku(Five in a Row, Omok) (5/5) - 3x3 체크 추가

2023.10.29 - [AI,ML,ALGORITHM] - Gomoku(Five in a Row, Omok) (5/5) - 머신러닝으로 게임 구현

 

반응형

'개발 > Note' 카테고리의 다른 글

BLE Advertising Payload format 샘플 분석  (1) 2024.01.11
Packet Format for the LE Uncoded PHYs  (0) 2024.01.09
[Mac OS X] Apache Virtual Hosts 설정 설정  (0) 2023.02.21
Unwind Segue 사용방법  (0) 2023.01.11
랜덤 seed 초기화  (0) 2022.11.18
블로그 이미지

SKY STORY

,
반응형

APNs 이란?

APNs(Apple Push Notification Service)는 Apple Device에 Push를 보내줄 수 있는 서비스이다. 지원하는 방식으로는 다음과 같다.

 

Legacy Push

- TLS(SSL) 소켓 통신

- Production(gateway.push.apple.com:2195) / Development(gateway.sandbox.push.apple.com:2195) 

Push

- HTTP/2 통신

- Production(api.push.apple.com:443) / Development(api.sandbox.push.apple.com:443) HTTP/1.1 통신을 지원하지 않는다.

 

APNS 기능

기본적으로 Push Notification 은 단방향 서비스이다.

개발 서버에서 APNS 에 푸쉬 알림을 요청하면 APNS가 알아서 디바이스에 전송한다.

개발 서버는 APNS에 요청하면 할 일은 끝이다.

개발 서버는 APNS 에 푸쉬 알림을 요청할 때, 데이터 format 등에 대한 정상 여부는 리턴 받지 만 실제 도착여부에 대해서는 리턴을 받지 못한다.

사용자가 설정에서 알림을 끄더라도 알림이 출력되지 않지만 알림 수신은 된다.

사용자가 알림을 중지했는지 여부는 알 수 없다. (즉 앱에서만 알 수 있음)

 

 

APNS 유실되는 사례

디바이스 전원이 꺼져있는 경우 APNS의 QoS (Quality of Service) 컴포넌트는 디바이스 전원이 꺼져있는 경우, Notification을 잠시 저장했다가 다시 사용 가능해질 때 Notification이 전달되도록 하지만, 다 음의 경우 저장된 Notification이 폐기된다.

 

- 잠시 꺼져 있는 경우(얼마나 잠시인지 명시되지 않았음) , APNS는 Notification을 저장을

하는데 저장 가능한 Notificaiton은 하나다. 디바이스가 꺼졌있는 동안 하나의 앱에서 여러

개의 푸쉬 알림을 보낸다면, 제일 나중에 보내진 Notification만 저장되어 전달된다. 이전

에 저장된 Notification 은 폐기된다.

 - 장시간 꺼져 있는 경우 (장시간이 얼마나인지는 명시되지 않았음), 저장된 Notification은 폐기된다.

 

APNS 서버 응답 코드

- 200 성공 

- 400 잘못된 요청 

- 403 인증서 또는 공급자 인증 토큰에 오류가 발생했습니다.

- 405 요청에 bad : 메서드 값이 사용되었습니다. POST 요청 만 을 지원합니다. 

- 410 장치 토큰이 해당 항목에 대해 더 이상 활성화되지 않습니다.

- 413 알림 페이로드가 너무 큽니다.

- 429 서버가 동일한 장치 토큰에 대해 너무 많은 요청을 수신했습니다.

- 500 내부 서버 오류 503 서버가 종료되어 사용할 수 없습니다.

- 503 서버가 종료되어 사용할 수 없습니다.

 

 

Feedback Service

Feedback 서비스는 Notification 전달 실패 정보를 개발 서버에 알려주기 위한 서비스다. 푸쉬 알림이 앱이 삭제된 이유로 전달되지 못했다면 , APNS 는 이를 Feedback 서비스에 알리 고 Feedback 서비스는 실패한 디바이스의 토큰을 리스트에 저장한다.

APNS 에서 푸쉬 알림을 디바이스로 전송하기 전에 실패한 – data format error 등 – 경우에 는 Feedback 서비스의 리스트에 등록되지 않는다.(즉, 전송자체가 성공한경우 Feedback 서 비스 사용이 가능함) 개발 서버는 APNS 연결과는 별개로 Feedback 서비스에 접속해서 디바이스 토큰 리스트를 요 청해야 한다. Feedback 서비스는 리스트가 요청될 때마다 리스트를 전달한 후 리스트를 초기화 (clear) 한다.

Feedback 서비스로부터 받은 리스트의 데이터에는 timestamp 정보가 포함되어 있다. 이를 이용하여 리스트에 있는 디바이스 토큰이 Push Service에 재등록되어 있는지를 확인할 수 있 다. (푸쉬 알림을 보낸 시각과 디바이스 토큰의 timestamp 를 비교하면 디바이스 토큰의 재등록 여부를 확인할 수 있다.)

 

Feedback 서비스로의 요청을 위한 url, port 및 format 등의 내용은 아래 링크 참조.

https://developer.apple.com/library/archive/documentation/NetworkingInternet/ Conceptual/RemoteNotificationsPG/BinaryProviderAPI.html

 

 

2020/05/19 - [개발노트] - UUID의 구성 요소

2020/05/18 - [분류 전체보기] - Multiple font colors in a single UILabel

2020/05/18 - [개발툴/Xcode] - Storyboard References (스토리보드 분리)

2020/05/18 - [iOS/Jailbreak] - OpenSSL Mac 연동

2020/05/18 - [iOS/Objective-C] - NSLog 출력 크기 제한 풀기

2020/05/18 - [OS/Mac OS X] - Symbolic Link

2020/05/18 - [개발툴/Xcode] - Release 모드에서 디버깅

2020/05/18 - [iOS/Jailbreak] - 탈옥후 안정화

2020/05/15 - [iOS/Swift] - 다중 문자열 / 캐릭터 제거

2020/05/15 - [iOS/Swift] - String substring

2020/05/15 - [iOS/Swift] - Framework 경로

2020/05/15 - [iOS/Objective-C] - Frameworks 경로

2020/05/15 - [iOS/Objective-C] - iOS디바이스 설정창 이동

2020/05/15 - [iOS/Objective-C] - Xcode 한글 깨짐 복구

 

반응형

'개발 > iOS' 카테고리의 다른 글

Frida 설치 및 사용법  (0) 2020.05.19
Tcpdump 사용법  (0) 2020.05.19
Multiple font colors in a single UILabel  (0) 2020.05.18
OpenSSL Mac 연동  (0) 2020.05.18
NSLog 출력 크기 제한 풀기  (0) 2020.05.18
블로그 이미지

SKY STORY

,