[PYTHON] Rendre le serveur SIP aussi concis que possible (au milieu de l'explication)

J'étais ingénieur VoIP côté serveur et côté signal de contrôle, donc lors de l'apprentissage d'une nouvelle langue, je crée d'abord un serveur SIP.

Dans cet article, j'écrirai que si vous implémentez au moins autant, vous pouvez fonctionner comme un serveur SIP si l'autre partie suit correctement la RFC3261.

Le langage utilise python. C'est 2.7 parce que c'est un code que j'ai écrit il y a environ 10 ans. Je pense que ce serait plus cool s'il était écrit par un programmeur brillant, mais cela fonctionne toujours, alors lisez-le s'il vous plaît.

Réception du signal SIP

Selon RFC3261, TCP doit être pris en charge, mais UDP doit également être pris en charge, alors attendez-vous à ce que l'autre partie parle avec UDP et ne prenne en charge que UDP. À Avec UDP, vous n'avez fondamentalement pas à vous soucier des coupures de signal (avec TCP, les données circulent lentement, vous devez donc voir exactement où les signaux sont séparés), c'est donc efficace pour simplifier le programme. est.

Je pense que c'est la même chose pour la plupart des langues, mais je vais créer un socket pour UDP, lier ce socket à l'IP et au port, puis continuer à recevoir sur ce socket. Ceci est l'image dans le code.

class Proxy:

  def __init__(self, ip, port):
    self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    self.sock.bind((ip, port))
    
  def run(self):
    while True:
      buf, addr = self.sock.recvfrom(0xffff)
      #Celui-ci contient le signal reçu par buf et l'adresse de l'autre partie envoyée à addr.

C'est facile. Cette prise sera également utilisée pour la transmission du signal dans un endroit différent La taille du tampon de réception est définie sur 0xffff car elle ne peut être reçue que jusqu'à cette taille en raison des spécifications UDP. (Étant donné que la taille comprend la partie en-tête du paquet UDP, la taille des données pouvant être reçues est un peu plus petite, mais en un coup d'œil, 0xffff est plus facile à voir, donc je m'en fiche)

Analyse du signal SIP (à peu près pour le moment)

Analysez grossièrement le signal reçu pour faciliter le traitement ultérieur. Personnellement, j'emballe des techniques secrètes ici. Eh bien, c'est peut-être quelque chose auquel tout le monde peut penser à condition de faire des recherches.

De plus, comme il est supposé que l'autre partie enverra le bon signal SIP, si vous visez et l'envoyez, vous pouvez provoquer un bug.

L'image entière est comme ça.

class Message:

  def __init__(self, buf):
    buf = re.sub(r'^((\r\n)|(\r)|(\n))*', "", buf)
    m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
    self.body = buf[m.end():]
    buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', "\n", buf[:m.start()]))
    ary = buf.split("\n")
    m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
    self.method, self.requri, self.stcode, self.reason = \
      m.group(2), m.group(3), m.group(5), m.group(6)
    self.hdrs = []
    for buf in ary[1:]:
      name, buf = re.split(r'\s*:\s*', buf, 1)
      self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

Je vais expliquer chaque partie.

En regardant RFC3261, il est permis d'avoir plusieurs CRLF au début du signal SIP. CRLF est soit \ r, \ n ou \ r \ n. Ce ne sont pas des informations significatives, je vais donc les supprimer pour le moment. \ R \ n, \ r ou \ n peut être écrit sous la forme d'une expression régulière Python.

r'(\r\n)|(\r)|(\n)'

De plus, je préfère écrire plus de parenthèses que nécessaire pour pouvoir comprendre clairement différentes choses.

S'il y en a 0 ou plus

r'((\r\n)|(\r)|(\n))*'

Je t'appellerai. Si vous ajoutez la condition au début

r'^((\r\n)|(\r)|(\n))*'

Sera. Si vous utilisez cette expression régulière pour remplacer buf,

buf = re.sub(r'^((\r\n)|(\r)|(\n))*', '', buf)

Ce sera.

Ensuite, trouvez la ligne vide qui sépare l'en-tête et le corps et divisez-la en deux.

La ligne vide est exprimée sous la forme de deux CRLF consécutifs, qui peuvent être exprimés sous la forme d'une expression régulière.

r'((\r\n\r\n)|(\r\r)|(\n\n))'

Si vous recherchez cette chaîne de caractères à partir du signal SIP et découpez la partie derrière elle comme corps

m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
self.body = buf[m.end():]

est devenu.

Jusqu'à présent, nous avons considéré CRLF comme \ r \ n, \ r ou \ n, mais c'est un problème, nous allons donc le remplacer par \ n ici.

Alors n'aurions-nous pas dû le remplacer plus tôt? Cependant, si la partie du corps peut contenir des informations qui ont du sens d'être \ r \ n, et si vous voulez voir la longueur des données correctement à l'aide de l'en-tête Content-Length. , Ce serait un problème si le corps est réécrit, alors ne remplacez que les parties autres que le corps.

Il sera remplacé par \ n, donc ce sera \ r \ n ou \ r. Cela signifie qu'il peut y avoir ou non \ r suivi de \ n, donc si vous exprimez cela avec une expression régulière,

r'¥r¥n?'

n'est-ce pas.

J'ai découvert la quantité de données reçues juste avant qu'il ne s'agisse de l'en-tête, donc sur cette base,

re.sub(r'\r\n?', '\n', buf[:m.start()])

Peut être remplacé par.

Soit dit en passant, l'en-tête SIP est mal conçu pour que CRLF puisse être inclus au milieu. Le CRLF au milieu n'a pas de sens, c'est juste pour l'apparence. Qui serait content de ça? Ce ne sera pas un obstacle lors du traitement, je veux donc l'effacer d'une manière ou d'une autre. Heureusement, s'il y a un CRLF au milieu de l'en-tête, c'est une règle de mettre WSP (caractère vide, un ou plusieurs espaces ou tabulations demi-largeur) après lui. Puisque CRLF a été remplacé par \ n dans le processus précédent, \ n suivi d'un ou plusieurs espaces ou tabulations sera remplacé par un seul espace demi-largeur comprenant cet espace ou cet onglet.

L'expression régulière est

r'¥n[ ¥t]+'

n'est-ce pas.

Quant au processus de conversion du CRLF que j'ai écrit plus tôt en \ n, je l'écrirai sur une ligne.

buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', '\n', buf[:m.start()]))

Ce sera beau. Maintenant que l'en-tête et la ligne de départ qui le précède sont déjà contenus dans une ligne séparée par \ n, divisez-les en un tableau.

ary = buf.split("\n")

Il est préférable de traiter chaque élément de cette zone. L'élément zéro est la ligne de départ et le premier élément et les éléments suivants sont l'en-tête.

Ensuite, analysez la ligne de départ. Il existe deux types de Start-Line, Request-Line et Status-Line.

Request-Line a une forme dans laquelle la méthode, Request-URI et SIP-Version sont alignés avec un espace demi-largeur entre les deux, la méthode a plusieurs alphabets demi-largeur supérieure et inférieure et Request-URI autorise divers caractères. Cependant, les espaces au moins demi-largeur ne sont pas autorisés.

r'([A-Z]+) ([^ ]+) SIP\/2\.0'

Status-Line est une forme dans laquelle SIP-Version, Status-Code et Reason-Phrase sont alignés avec un espace demi-largeur entre les deux.

r'SIP\/2\.0 (\d+) ([^\n]+)'

À l'exception du dernier saut de ligne, le saut de ligne a déjà disparu en divisant chaque saut de ligne, il n'y a donc pas de problème même s'il est analysé comme une chaîne de caractères arbitraire. Ne faites pas d'erreur lorsque vous copiez et collez dans un endroit où vous ne pouvez pas supposer qu'il n'y a pas de saut de ligne.

Request-Line a SIP-Version à la fin, et Status-Line a SIP-Version au début, de sorte que vous pouvez facilement tout analyser à la fois.

r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?'

Si vous analysez en utilisant ceci et conservez le résultat

m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
self.method, self.requri, self.stcode, self.reason = m.group(2), m.group(3), m.group(5), m.group(6)

Continuez à analyser l'en-tête. Puisque les données brutes de l'en-tête sont incluses à partir de la première ligne du tableau appelé ary,

for buf in ary[1:]:
  pass #Analysez le buf ici

Je vais l'analyser comme ça. L'en-tête a un nom d'en-tête, HCOLON, suivi de la valeur d'en-tête. HCOLON signifie qu'il peut y avoir ou non un espace avant et après les deux points (:). Alors, commencez par le diviser par une chaîne de caractères avec des espaces avant et après les deux points.

for buf in ary[1:]:
  name, buf = re.split(r'\s*:\s*', buf, 1)

Puisque les éléments de la valeur d'en-tête sont séparés par des virgules, séparez-les ici.

re.split(r'\s*,\s*', buf)

Cependant, ce n'est pas vraiment cool, et c'est bogué si certaines des valeurs d'en-tête contiennent des virgules dans la chaîne double quart. Vous pouvez le faire en écrivant une expression régulière légèrement plus difficile.

Créez une classe comme celle-ci pour stocker les résultats de l'analyse

class Header:
  def __init__(self, name, vals):
    self.name, self.vals = name, vals

J'ai décidé de mettre le résultat de l'analyse d'en-tête dans ce

self.hdrs = []
for buf in ary[1:]:
  name, buf = re.split(r'\s*:\s*', buf, 1)
  self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

Au fait, si vous les collez tous ensemble

class Message:

  def __init__(self, buf):
    buf = re.sub(r'^((\r\n)|(\r)|(\n))*', "", buf)
    m = re.search(r'((\r\n\r\n)|(\r\r)|(\n\n))', buf)
    self.body = buf[m.end():]
    buf = re.sub(r'\n[ \t]+',' ', re.sub(r'\r\n?', "\n", buf[:m.start()]))
    ary = buf.split("\n")
    m = re.match(r'(([A-Z]+) ([^ ]+) )?SIP\/2\.0( (\d+) ([^\n]+))?', ary[0])
    self.method, self.requri, self.stcode, self.reason = \
      m.group(2), m.group(3), m.group(5), m.group(6)
    self.hdrs = []
    for buf in ary[1:]:
      name, buf = re.split(r'\s*:\s*', buf, 1)
      self.hdrs.append(Header(name, re.split(r'\s*,\s*', buf)))

est devenu.

Un paragraphe

Je suis fatigué, alors je continuerai la prochaine fois. Je suis épuisé et l'explication est déjà appropriée, je vais donc la mettre à jour bientôt. Le produit fini est le suivant.

https://github.com/zurustar/xylitol/blob/master/xylitol.py

Ajustez la priorité avec d'autres mises à jour d'article en fonction du nombre de likes

Recommended Posts

Rendre le serveur SIP aussi concis que possible (au milieu de l'explication)
Copiez la liste en Python
Rendre la progression de dd visible sur la barre de progression
Au milieu du développement, nous présenterons Alembic
Rendre la valeur par défaut de l'argument immuable (explication de l'article)
L'explication la plus facile à comprendre au monde sur la création de LINE BOT (3) [Coopération avec un serveur avec Git]
Maintenance de l'environnement de développement Django + MongoDB (en cours d'écriture)
Rendre la fonction de dessin de polices japonaises dans OpenCV en général
Supprimer les sauts de page au milieu d'un tableau avec sphinx single html
L'histoire de la participation à AtCoder
L'histoire du "trou" dans le fichier
L'histoire du remontage du serveur d'application
Supplément à l'explication de vscode
Explication et implémentation du protocole XMPP utilisé dans Slack, HipChat et IRC
Si vous voulez un singleton en python, considérez le module comme un singleton
[Introduction à Python] Une explication approfondie des types de chaînes de caractères utilisés dans Python!